openapi: 3.1.0
info:
  title: Site Spy API
  description: >
    # Site Spy web page monitoring and notifications API


    REST API for managing page watches, group tags, and notifications.


    Site Spy can be driven by this simple REST API — the endpoint pages include
    `curl`, Python, and JavaScript examples to help you get started faster.


    ## Where to find my API key?


    Your API key is under [**Settings →
    API**](https://sitespy.app/dashboard/settings?tab=api) in the Site Spy
    dashboard.

    Simply click the API key to copy it to your clipboard.


    ## Connection URL


    All endpoints live under `https://detect.coolify.vkuprin.com/api/v1`, for
    example:

    `https://detect.coolify.vkuprin.com/api/v1/watch/cc0cfffa-f449-477b-83ea-0caafd1dc091/history`


    ## Authentication


    Almost all API requests require authentication, provided as an **API key**
    in the header of the HTTP request.


    For example: `x-api-key: YOUR_API_KEY`
  version: 0.1.6
  contact:
    name: Site Spy
    url: https://sitespy.app
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
  - url: https://detect.coolify.vkuprin.com/api/v1
    description: Site Spy production
security:
  - ApiKeyAuth: []
tags:
  - name: Watch Management
    description: >
      Core functionality for managing web page monitors. Create, retrieve,
      update, and delete individual watches.

      Each watch represents a single URL being monitored for changes, with
      configurable settings for check intervals,

      notification preferences, and content filtering options.
  - name: Watch History
    description: |
      Get a list of timestamps of all changes detected for a watch.
  - name: Snapshots
    description: >
      Retrieve individual text snapshot of monitored content according to the
      `timestamp`. The text snapshot is the HTML

      to Text at page check time.


      Set the query argument `html` to any value to retrieve the last HTML
      fetched, the system only keeps the last two

      (2) HTML files fetched.


      Use the Watch History API endpoint to get a list of timestamps to pass to
      this query.
  - name: Favicon
    description: >
      Retrieve favicon images associated with monitored web pages. These are
      used in the dashboard interface

      to visually identify different watches in your monitoring list.
  - name: Tag Management
    description: >
      Organize your watches using tags and groups. Tags (also known as Groups)
      allow you to categorize monitors, set group-wide

      notification preferences, and perform bulk operations like mass rechecking
      or status changes across

      multiple related watches.
  - name: Notifications
    description: >
      Configure global notification endpoints that can be used across all your
      watches. Supports various

      notification services including email, Discord, Slack, webhooks, and many
      other popular platforms.

      These settings serve as defaults that can be overridden at the individual
      watch or tag level.


      The notification syntax uses
      [https://github.com/caronc/apprise](https://github.com/caronc/apprise).
  - name: User Settings
    description: >
      Manage user-specific settings such as default recheck intervals. These
      settings are stored per API key

      and apply to all watches created by that user unless overridden at the
      watch level.
  - name: Search
    description: >
      Search and filter your watches by URL patterns, titles, or tags. Useful
      for quickly finding specific

      monitors in large collections or identifying watches that match certain
      criteria.
  - name: Import
    description: >
      Bulk import multiple URLs for monitoring. Accepts plain text lists of URLs
      and can automatically

      apply tags, proxy settings, and other configurations to all imported
      watches simultaneously.
  - name: System Information
    description: >
      Retrieve system status and statistics about your Site Spy instance,
      including total watch

      counts, uptime information, and version details.
  - name: Email Verification
    description: >
      Manage email notification settings for your API key. Each API key can have
      one email address

      associated with it for receiving notifications. The email must be verified
      before notifications

      can be sent to it.
  - name: Telegram Notifications
    description: >
      Connect your Telegram account to receive instant change notifications.

      Generate a connect link, open it in Telegram, tap Start — done. Free for
      all plans.
  - name: Web Push Notifications
    description: >
      Subscribe to browser push notifications for instant alerts when watched
      pages change.

      Uses the Web Push protocol (RFC 8030) with VAPID authentication. Free for
      all plans.
  - name: RSS Feeds
    description: >
      Manage per-user RSS feed access tokens and retrieve feed URLs. Each user
      can generate a

      personal RSS token that provides access to their watches via standard RSS
      readers. Feeds

      are scoped to the user's watches only, ensuring multi-tenant privacy.
  - name: API Key Management
    description: >
      Generate and manage API keys. API keys are used to authenticate all
      requests to the API.

      Each API key is associated with a unique user and their watches.
  - name: Feature Flags
    description: >
      Retrieve feature flags for the current API key. Feature flags control
      which features are

      enabled or disabled for specific users.
  - name: Authentication
    description: >
      User account registration and authentication. Users can create accounts
      with email/password

      and receive an API key that links to their account. This enables syncing
      watches across

      the browser extension and web dashboard.
  - name: Plan
    description: >
      Retrieve the current subscription plan and usage for the authenticated API
      key. Plans control

      the maximum number of watches a user can create (Free: 2, Starter: 25,
      Pro: 100).
  - name: Billing
    description: >
      Manage subscriptions via Lemon Squeezy. Create checkout sessions to
      upgrade, access the

      customer billing portal, and receive webhook events from Lemon Squeezy.
  - name: Feedback
    description: >
      Collect product research responses from authenticated users. One-time PMF
      (Sean Ellis)

      survey submission per user.
  - name: Screenshots
    description: >
      Retrieve the history of visual snapshots captured for a watch. Use the
      returned timestamps

      with the Visual Diff endpoints to compare any two captures.
  - name: Visual Diff
    description: >
      Retrieve individual screenshots and pixel-level visual comparisons between
      any two captured

      snapshots of a watch, including difference metadata such as changed
      regions.
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        API key for authentication. You can find your API key in the Site Spy
        dashboard under Settings → API.
  schemas:
    TimeBetweenCheck:
      type: object
      properties:
        weeks:
          type: integer
          minimum: 0
        days:
          type: integer
          minimum: 0
        hours:
          type: integer
          minimum: 0
        minutes:
          type: integer
          minimum: 0
        seconds:
          type: integer
          minimum: 0
      description: Time intervals between checks
    EmailNotifications:
      type: object
      properties:
        enabled:
          type: boolean
          description: Whether email notifications are enabled for this user
        email:
          type: string
          format: email
          description: Email address to send notifications to
      description: User email notification preferences
    WatchBase:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier
          readOnly: true
        date_created:
          type:
            - integer
            - 'null'
          description: Unix timestamp of creation
          readOnly: true
        owner_api_key:
          type:
            - string
            - 'null'
          description: API key that owns this watch (multi-tenant isolation)
          readOnly: true
        url:
          type: string
          format: uri
          description: URL to monitor for changes
          maxLength: 5000
        title:
          type:
            - string
            - 'null'
          description: >-
            Custom title for the web page change monitor (watch), not to be
            confused with page_title
          maxLength: 5000
        tag:
          type: string
          description: Tag UUID to associate with this web page change monitor (watch)
          maxLength: 5000
        tags:
          type: array
          items:
            type: string
          description: Array of tag UUIDs
        paused:
          type: boolean
          description: Whether the web page change monitor (watch) is paused
        paused_by:
          type:
            - string
            - 'null'
          enum:
            - plan_limit
            - null
          description: >
            Non-null when `paused=true` was set automatically. Values:
              - `plan_limit` — owner exceeded their plan's watch cap; auto-deleted 30d after `paused_at` unless user upgrades or un-pauses.
            Null means the watch was paused manually by the user (no
            auto-delete).
          readOnly: true
        paused_at:
          type: integer
          description: >
            Unix timestamp when auto-pause was applied. Starts the auto-delete
            clock for

            `paused_by != null` watches. 0 when never auto-paused.
          readOnly: true
        notification_muted:
          type: boolean
          description: Whether notifications are muted
        method:
          type: string
          enum:
            - GET
            - POST
            - DELETE
            - PUT
          description: HTTP method to use
        fetch_backend:
          type: string
          description: |
            Backend to use for fetching content. Common values:
            - `system` (default) - Use the system-wide default fetcher
            - `html_requests` - Fast requests-based fetcher
            - `html_webdriver` - Browser-based fetcher (Playwright/Puppeteer)
            - `extra_browser_*` - Custom browser configurations (if configured)
            - Plugin-provided fetchers (if installed)
          pattern: ^(system|html_requests|html_webdriver|extra_browser_.+)$
          default: system
        headers:
          type: object
          additionalProperties:
            type: string
          description: HTTP headers to include in requests
        body:
          type:
            - string
            - 'null'
          description: HTTP request body
          maxLength: 5000
        proxy:
          type:
            - string
            - 'null'
          description: Proxy configuration
          maxLength: 5000
        ignore_status_codes:
          type:
            - boolean
            - 'null'
          description: Ignore HTTP status code errors (boolean or null)
        webdriver_delay:
          type:
            - integer
            - 'null'
          description: Delay in seconds for webdriver
        webdriver_js_execute_code:
          type:
            - string
            - 'null'
          description: JavaScript code to execute
          maxLength: 5000
        time_between_check:
          type: object
          properties:
            weeks:
              type:
                - integer
                - 'null'
              minimum: 0
              maximum: 52000
            days:
              type:
                - integer
                - 'null'
              minimum: 0
              maximum: 365000
            hours:
              type:
                - integer
                - 'null'
              minimum: 0
              maximum: 8760000
            minutes:
              type:
                - integer
                - 'null'
              minimum: 0
              maximum: 525600000
            seconds:
              type:
                - integer
                - 'null'
              minimum: 0
              maximum: 31536000000
          description: >-
            Time intervals between checks. All fields must be non-negative. At
            least one non-zero value required when not using default settings.
        time_between_check_use_default:
          type: boolean
          default: true
          description: >-
            Whether to use global settings for time between checks - defaults to
            true if not set
        notification_urls:
          type: array
          items:
            type: string
            maxLength: 1000
          maxItems: 100
          description: >-
            Notification URLs for this web page change monitor (watch). Maximum
            100 URLs.
        notification_title:
          type:
            - string
            - 'null'
          description: Custom notification title
          maxLength: 5000
        notification_body:
          type:
            - string
            - 'null'
          description: Custom notification body
          maxLength: 5000
        notification_format:
          type: string
          enum:
            - text
            - html
            - htmlcolor
            - markdown
            - System default
          description: Format for notifications
        track_ldjson_price_data:
          type:
            - boolean
            - 'null'
          description: Whether to track JSON-LD price data
        browser_steps:
          type: array
          items:
            type: object
            properties:
              operation:
                type:
                  - string
                  - 'null'
                maxLength: 5000
              selector:
                type:
                  - string
                  - 'null'
                maxLength: 5000
              optional_value:
                type:
                  - string
                  - 'null'
                maxLength: 5000
            required:
              - operation
              - selector
              - optional_value
            additionalProperties: false
          maxItems: 100
          description: Browser automation steps. Maximum 100 steps allowed.
        processor:
          type: string
          enum:
            - restock_diff
            - text_json_diff
            - image_ssim_diff
          default: text_json_diff
          description: >-
            Optional processor mode to use for change detection.
            `image_ssim_diff` (screenshot monitoring) requires Pro plan.
            Defaults to `text_json_diff` if not specified.
        screenshot_enabled:
          type: boolean
          default: false
          description: >-
            When true, capture and store screenshots alongside text snapshots on
            each check. Requires Pro plan and `fetch_backend` set to a
            browser-based fetcher (e.g. `html_webdriver`).
        include_filters:
          type: array
          items:
            type: string
            maxLength: 5000
          maxItems: 100
          description: CSS/XPath selectors to extract specific content from the page
        subtractive_selectors:
          type: array
          items:
            type: string
            maxLength: 5000
          maxItems: 100
          description: CSS/XPath selectors to remove content from the page
        ignore_text:
          type: array
          items:
            type: string
            maxLength: 5000
          maxItems: 100
          description: Text patterns to ignore in change detection
        trigger_text:
          type: array
          items:
            type: string
            maxLength: 5000
          maxItems: 100
          description: Text/regex patterns that must be present to trigger a change
        text_should_not_be_present:
          type: array
          items:
            type: string
            maxLength: 5000
          maxItems: 100
          description: Text that should NOT be present (triggers alert if found)
        extract_text:
          type: array
          items:
            type: string
            maxLength: 5000
          maxItems: 100
          description: Regex patterns to extract specific text after filtering
        trim_text_whitespace:
          type: boolean
          default: false
          description: Strip leading/trailing whitespace from text
        sort_text_alphabetically:
          type: boolean
          default: false
          description: Sort lines alphabetically before comparison
        remove_duplicate_lines:
          type: boolean
          default: false
          description: Remove duplicate lines from content
        check_unique_lines:
          type: boolean
          default: false
          description: Compare against all history for unique lines
        strip_ignored_lines:
          type:
            - boolean
            - 'null'
          description: Remove lines matching ignore patterns
        filter_text_added:
          type: boolean
          default: true
          description: Include added text in change detection
        filter_text_removed:
          type: boolean
          default: true
          description: Include removed text in change detection
        filter_text_replaced:
          type: boolean
          default: true
          description: Include replaced text in change detection
        in_stock_only:
          type: boolean
          default: true
          description: Only trigger on in-stock transitions (restock_diff processor)
        follow_price_changes:
          type: boolean
          default: true
          description: Monitor and track price changes (restock_diff processor)
        price_change_threshold_percent:
          type:
            - number
            - 'null'
          description: Minimum price change percentage to trigger notification
        has_ldjson_price_data:
          type:
            - boolean
            - 'null'
          description: Whether page has LD-JSON price data (auto-detected)
          readOnly: true
        notification_screenshot:
          type: boolean
          default: false
          description: >-
            Include screenshot in notifications (if supported by notification
            URL)
        filter_failure_notification_send:
          type: boolean
          default: true
          description: Send notification when filters fail to match content
        use_page_title_in_list:
          type:
            - boolean
            - 'null'
          description: Display page title in watch list (null = use system default)
        history_snapshot_max_length:
          type:
            - integer
            - 'null'
          minimum: 1
          maximum: 1000
          description: >-
            Maximum number of history snapshots to keep (null = use system
            default)
        time_schedule_limit:
          type: object
          description: Weekly schedule limiting when checks can run
          properties:
            enabled:
              type: boolean
              default: false
            monday:
              $ref: '#/components/schemas/DaySchedule'
            tuesday:
              $ref: '#/components/schemas/DaySchedule'
            wednesday:
              $ref: '#/components/schemas/DaySchedule'
            thursday:
              $ref: '#/components/schemas/DaySchedule'
            friday:
              $ref: '#/components/schemas/DaySchedule'
            saturday:
              $ref: '#/components/schemas/DaySchedule'
            sunday:
              $ref: '#/components/schemas/DaySchedule'
        conditions:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
                description: Field to check (e.g., 'page_filtered_text', 'page_title')
              operator:
                type: string
                description: >-
                  Comparison operator (e.g., 'contains_regex', 'equals',
                  'not_equals')
              value:
                type: string
                description: Value to compare against
            required:
              - field
              - operator
              - value
          maxItems: 100
          description: >-
            Array of condition rules for change detection logic (empty array
            when not set)
        conditions_match_logic:
          type: string
          enum:
            - ALL
            - ANY
          default: ALL
          description: >-
            Logic operator - ALL (match all conditions) or ANY (match any
            condition)
        ai_alert_condition:
          type:
            - string
            - 'null'
          description: >-
            Plain-English condition for AI smart alerts (Pro+ only). When set,
            AI evaluates each change against this condition and suppresses
            notifications that don't match. Example - "Only notify me if the
            price drops below $50".
          maxLength: 500
        last_ai_summary:
          type:
            - string
            - 'null'
          description: >-
            AI-generated plain-English summary of the most recent change (Pro+
            only, read-only — generated automatically when a change is
            detected).
          readOnly: true
        track_what:
          type:
            - string
            - 'null'
          description: >-
            User's monitoring intent, e.g. "price changes", "new job listings".
            Set by smart-configure or AI chat when the user describes what they
            want to track.
          maxLength: 500
    DaySchedule:
      type: object
      properties:
        enabled:
          type: boolean
          default: true
        start_time:
          type: string
          pattern: ^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$
          default: '00:00'
          description: Start time in HH:MM format
        duration:
          type: object
          properties:
            hours:
              type: string
              pattern: ^[0-9]+$
              default: '24'
            minutes:
              type: string
              pattern: ^[0-9]+$
              default: '00'
    Watch:
      allOf:
        - $ref: '#/components/schemas/WatchBase'
        - type: object
          properties:
            last_checked:
              type: integer
              description: Unix timestamp of last check
              readOnly: true
            last_changed:
              type: integer
              description: Unix timestamp of last change
              readOnly: true
              x-computed: true
            last_error:
              type:
                - string
                - boolean
                - 'null'
              description: >-
                Last error message (false when no error, string when error
                occurred, null if not checked yet)
              readOnly: true
            last_viewed:
              type: integer
              description: >-
                Unix timestamp in seconds of the last time the watch was viewed.
                Setting it to a value higher than `last_changed` in the "Update
                watch" endpoint marks the watch as viewed.
              minimum: 0
            link:
              type: string
              format: string
              description: >-
                The watch URL rendered in case of any Jinja2 markup, always use
                this for listing.
              readOnly: true
              x-computed: true
            page_title:
              type:
                - string
                - 'null'
              description: HTML <title> tag extracted from the page
              readOnly: true
            check_count:
              type: integer
              description: Total number of checks performed
              readOnly: true
            fetch_time:
              type: number
              description: Duration of last fetch in seconds
              readOnly: true
            previous_md5:
              type:
                - string
                - boolean
              description: MD5 hash of previous content (false if not set)
              readOnly: true
            previous_md5_before_filters:
              type:
                - string
                - boolean
              description: MD5 hash before filters applied (false if not set)
              readOnly: true
            consecutive_filter_failures:
              type: integer
              description: Counter for consecutive filter match failures
              readOnly: true
            filters_source:
              type: string
              description: >-
                Whether filters were set manually or by smart-configure
                ('manual' or 'smart')
              enum:
                - manual
                - smart
              default: manual
            smart_reconfigure_count:
              type: integer
              description: How many times smart-configure has re-run due to self-healing
              readOnly: true
            last_notification_error:
              type:
                - string
                - 'null'
              description: Last notification error message
              readOnly: true
            notification_alert_count:
              type: integer
              description: Number of notifications sent
              readOnly: true
            last_notification_delivery_reason:
              type:
                - string
                - 'null'
              description: >
                Why the most recent delivery attempt succeeded or failed.
                Populated on every

                change-detected event so the dashboard can explain a silent
                watch to the user.

                Possible values include `delivered_via_user_email`,
                `delivered_via_telegram`,

                `delivered_via_webpush`, `email_unverified`,
                `email_notifications_muted`,

                `smtp_not_configured`, `no_owner_api_key`, `no_urls_configured`.
              readOnly: true
            last_notification_delivery_ts:
              type: integer
              description: >-
                Unix timestamp of the most recent delivery attempt (success or
                silent drop)
              readOnly: true
            change_detected_but_not_delivered_count:
              type: integer
              description: >
                Incremented every time a change was detected but no notification
                was queued.

                A non-zero value combined with `notification_alert_count=0`
                means the detection

                engine is working but delivery is blocked — check
                `last_notification_delivery_reason`.
              readOnly: true
            content-type:
              type:
                - string
                - 'null'
              description: Content-Type from last fetch
              readOnly: true
            remote_server_reply:
              type:
                - string
                - 'null'
              description: Server header from last response
              readOnly: true
            browser_steps_last_error_step:
              type:
                - integer
                - 'null'
              description: Last browser step that caused an error
              readOnly: true
            viewed:
              type:
                - integer
                - boolean
              description: >-
                Computed property - true if watch has been viewed, false
                otherwise (deprecated, use last_viewed instead)
              readOnly: true
              x-computed: true
            history_n:
              type: integer
              description: Number of history snapshots available
              readOnly: true
              x-computed: true
            health:
              type: object
              readOnly: true
              x-computed: true
              description: >
                User-facing health summary. Computed from existing watch fields;
                no new

                storage. Lets the dashboard tell the user *which kind of broken*
                a watch

                is and offer a one-click fix.
              properties:
                state:
                  type: string
                  enum:
                    - healthy
                    - never_checked
                    - fetch_error
                    - fetch_suspicious
                    - filter_broken
                    - delivery_blocked
                  description: Single-word state for badge display.
                reason:
                  type: string
                  description: Human-readable explanation safe to show in UI.
                action:
                  type:
                    - string
                    - 'null'
                  enum:
                    - reconfigure_filter
                    - retry_fetch
                    - verify_email
                    - null
                  description: >-
                    Suggested next action; the dashboard maps this to a CTA
                    button.
                since_ts:
                  type: integer
                  description: Unix timestamp the current state was first observed.
                delivery_reason:
                  type:
                    - string
                    - 'null'
                  description: >
                    Present only when state=delivery_blocked. Mirrors

                    `last_notification_delivery_reason` so the UI can render
                    reason-specific copy.
                missed_deliveries:
                  type: integer
                  description: >
                    Present only when state=delivery_blocked. Number of changes
                    that were

                    detected but not delivered. Mirrors
                    `change_detected_but_not_delivered_count`.
    CreateWatch:
      allOf:
        - $ref: '#/components/schemas/WatchBase'
        - type: object
          required:
            - url
    UpdateWatch:
      allOf:
        - $ref: '#/components/schemas/WatchBase'
        - type: object
          properties:
            last_viewed:
              type: integer
              description: >-
                Unix timestamp in seconds of the last time the watch was viewed.
                Setting it to a value higher than `last_changed` in the "Update
                watch" endpoint marks the watch as viewed.
              minimum: 0
    Tag:
      allOf:
        - $ref: '#/components/schemas/WatchBase'
        - type: object
          properties:
            overrides_watch:
              type:
                - boolean
                - 'null'
              description: >
                Whether this tag's settings override watch settings for all
                watches in this tag/group.

                - true: Tag settings override watch settings

                - false: Tag settings do not override (watches use their own
                settings)

                - null: Not decided yet / inherit default behavior
    CreateTag:
      allOf:
        - $ref: '#/components/schemas/Tag'
        - type: object
          required:
            - title
    NotificationUrls:
      type: object
      properties:
        notification_urls:
          type: array
          items:
            type: string
            format: uri
          description: List of notification URLs
      required:
        - notification_urls
    SystemInfo:
      type: object
      properties:
        watch_count:
          type: integer
          description: Total number of web page change monitors (watches)
        tag_count:
          type: integer
          description: Total number of tags
        uptime:
          type: string
          description: System uptime
        version:
          type: string
          description: Application version
    SearchResult:
      type: object
      properties:
        watches:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Watch'
          description: >-
            Dictionary of matching web page change monitors (watches) keyed by
            UUID
    WatchHistory:
      type: object
      additionalProperties:
        type: string
        description: Path to snapshot file
      description: Dictionary of timestamps and snapshot paths
    Error:
      type: object
      properties:
        message:
          type: string
          description: Error message
paths:
  /watch:
    get:
      operationId: listWatches
      tags:
        - Watch Management
      summary: List all watches
      description: >-
        Return concise list of available web page change monitors (watches) and
        basic info
      parameters:
        - name: recheck_all
          in: query
          description: Set to 1 to force recheck of all watches
          schema:
            type: string
            enum:
              - '1'
        - name: tag
          in: query
          description: Tag name to filter results
          schema:
            type: string
      responses:
        '200':
          description: List of watches
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  $ref: '#/components/schemas/Watch'
              example:
                095be615-a8ad-4c33-8e9c-c7612fbf6c9f:
                  uuid: 095be615-a8ad-4c33-8e9c-c7612fbf6c9f
                  url: http://example.com?id={{1+1}} - the raw URL
                  link: >-
                    http://example.com?id=2 - the rendered URL, always use this
                    for listing.
                  title: Example Website Monitor - manually entered title/description
                  page_title: The HTML <title> from the page
                  tags:
                    - 550e8400-e29b-41d4-a716-446655440000
                  paused: false
                  notification_muted: false
                  method: GET
                  fetch_backend: html_requests
                  last_checked: 1640995200
                  last_changed: 1640995200
                7c9e6b8d-f2a1-4e5c-9d3b-8a7f6e4c2d1a:
                  uuid: 7c9e6b8d-f2a1-4e5c-9d3b-8a7f6e4c2d1a
                  url: http://example.com?id={{1+1}} - the raw URL
                  link: >-
                    http://example.com?id=2 - the rendered URL, always use this
                    for listing.
                  title: News Site Tracker - manually entered title/description
                  page_title: The HTML <title> from the page
                  tags:
                    - 330e8400-e29b-41d4-a716-446655440001
                  paused: false
                  notification_muted: true
                  method: GET
                  fetch_backend: html_webdriver
                  last_checked: 1640998800
                  last_changed: 1640995200
      x-codeSamples:
        - lang: curl
          source: |
            curl -X GET "https://detect.coolify.vkuprin.com/api/v1/watch" \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            response =
            requests.get('https://detect.coolify.vkuprin.com/api/v1/watch',
            headers=headers)

            print(response.json())
    post:
      operationId: createWatch
      tags:
        - Watch Management
      summary: Create a new watch
      description: >
        Create a single web page change monitor (watch). Requires at least `url`
        to be set.


        Every watch can be configured with:

        - **Processor mode**: `processor` field (`restock_diff` or
        `text_json_diff` - default)

        - **Notification settings**: `notification_urls` (array),
        `notification_title`, `notification_body`, `notification_format`,
        `notification_muted`

        - **Tags/Groups**: `tag` (UUID string) or `tags` (array of UUIDs)

        - **Check settings**: `time_between_check`, `paused`, `method`,
        `fetch_backend`

        - **Advanced options**: `headers`, `body`, `proxy`, `browser_steps`, and
        more
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWatch'
            example:
              url: https://example.com
              title: Example Site Monitor
              time_between_check:
                hours: 1
      responses:
        '200':
          description: Web page change monitor (watch) created successfully
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '403':
          description: Forbidden - email not verified or watch limit reached
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                      - email_not_verified
                      - watch_limit_reached
                    example: email_not_verified
                  message:
                    type: string
                    example: Please verify your email address before creating watches.
        '500':
          description: Server error
          content:
            text/plain:
              schema:
                type: string
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://detect.coolify.vkuprin.com/api/v1/watch" \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "url": "https://example.com",
                "title": "Example Site Monitor",
                "time_between_check": {
                  "hours": 1
                }
              }'
        - lang: Python
          source: >
            import requests

            import json


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            data = {
                'url': 'https://example.com',
                'title': 'Example Site Monitor',
                'time_between_check': {
                    'hours': 1
                }
            }

            response =
            requests.post('https://detect.coolify.vkuprin.com/api/v1/watch',
                                   headers=headers, json=data)
            print(response.text)
  /watch/{uuid}:
    get:
      operationId: getWatch
      tags:
        - Watch Management
      summary: Get single watch
      description: >-
        Retrieve web page change monitor (watch) information and set
        muted/paused status. Returns the FULL Watch JSON.
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
        - name: recheck
          in: query
          description: Recheck this web page change monitor (watch)
          schema:
            type: string
            enum:
              - '1'
              - 'true'
        - name: paused
          in: query
          description: Set pause state
          schema:
            type: string
            enum:
              - paused
              - unpaused
        - name: muted
          in: query
          description: Set mute state
          schema:
            type: string
            enum:
              - muted
              - unmuted
      responses:
        '200':
          description: Watch information or operation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Watch'
            text/plain:
              schema:
                type: string
                example: OK
        '404':
          description: Web page change monitor (watch) not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            response =
            requests.get(f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}',
            headers=headers)

            print(response.json())
    put:
      operationId: updateWatch
      tags:
        - Watch Management
      summary: Update watch
      description: >-
        Update an existing web page change monitor (watch) using JSON. Accepts
        the same structure as returned in [get single watch
        information](#operation/getWatch).
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWatch'
      responses:
        '200':
          description: Web page change monitor (watch) updated successfully
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '500':
          description: Server error
      x-codeSamples:
        - lang: curl
          source: >
            curl -X PUT
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
            \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "url": "https://updated-example.com",
                "title": "Updated Monitor",
                "paused": false
              }'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            data = {
                'url': 'https://updated-example.com',
                'title': 'Updated Monitor',
                'paused': False
            }

            response =
            requests.put(f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}',
                                  headers=headers, json=data)
            print(response.text)
    delete:
      operationId: deleteWatch
      tags:
        - Watch Management
      summary: Delete watch
      description: Delete a web page change monitor (watch) and all related history
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Web page change monitor (watch) deleted successfully
          content:
            text/plain:
              schema:
                type: string
                example: OK
      x-codeSamples:
        - lang: curl
          source: >
            curl -X DELETE
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            response =
            requests.delete(f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}',
            headers=headers)

            print(response.text)
  /watch/{uuid}/history:
    get:
      operationId: getWatchHistory
      tags:
        - Watch History
      summary: Get watch history
      description: >
        Get a list of all historical snapshots available for a web page change
        monitor (watch), use the key `timestamp`

        as the query argument for fetching a single watch history snapshot.
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: List of available snapshots
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WatchHistory'
              example:
                '1640995200': /path/to/snapshot1.txt
                '1640998800': /path/to/snapshot2.txt
        '404':
          description: Web page change monitor (watch) not found
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/history"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            response =
            requests.get(f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}/history',
            headers=headers)

            print(response.json())
  /watch/{uuid}/history/{timestamp}:
    get:
      operationId: getWatchSnapshot
      tags:
        - Snapshots
      summary: Get single snapshot
      description: >
        Get single snapshot from web page change monitor (watch). Use 'latest'
        for the most recent snapshot.

        Use the Watch History API to get a list of timestamps to pass.
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
        - name: timestamp
          in: path
          required: true
          description: Snapshot timestamp or 'latest'
          schema:
            oneOf:
              - type: integer
              - type: string
                enum:
                  - latest
        - name: html
          in: query
          description: Set to 1 to return the last HTML
          schema:
            type: string
            enum:
              - '1'
      responses:
        '200':
          description: Snapshot content
          content:
            text/plain:
              schema:
                type: string
        '404':
          description: Snapshot not found
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/history/latest"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            timestamp = 'latest'  # or use specific timestamp like 1640995200

            response =
            requests.get(f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}/history/{timestamp}',
            headers=headers)

            print(response.text)
  /watch/{uuid}/screenshot/{timestamp}:
    get:
      operationId: getWatchScreenshot
      tags:
        - Visual Diff
      summary: Get a screenshot for a watch at a specific timestamp
      description: >
        Returns the screenshot image (PNG/JPEG) for a watch that uses the
        `image_ssim_diff` processor.

        Requires Pro plan.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: timestamp
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Screenshot image
          content:
            image/png:
              schema:
                type: string
                format: binary
        '400':
          description: Watch does not use screenshot monitoring
        '403':
          description: Pro plan required
        '404':
          description: Watch or snapshot not found
  /watch/{uuid}/screenshot/diff:
    get:
      operationId: getWatchScreenshotDiff
      tags:
        - Visual Diff
      summary: Get rendered diff overlay image
      description: >
        Returns a rendered diff image (JPEG) showing changed areas highlighted
        in red.

        Compares two screenshot snapshots using OpenCV.

        Requires Pro plan.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: from_version
          in: query
          schema:
            type: string
        - name: to_version
          in: query
          schema:
            type: string
        - name: asset
          in: query
          description: Which asset to return (before, after, or rendered_diff)
          schema:
            type: string
            enum:
              - before
              - after
              - rendered_diff
            default: rendered_diff
      responses:
        '200':
          description: Diff overlay image
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
        '400':
          description: Watch does not use screenshot monitoring
        '403':
          description: Pro plan required
        '404':
          description: Could not generate diff
  /watch/{uuid}/screenshot/diff/metadata:
    get:
      operationId: getWatchScreenshotDiffMetadata
      tags:
        - Visual Diff
      summary: Get visual diff metadata
      description: >
        Returns metadata about the visual diff comparison including change
        percentage,

        threshold setting, and available versions.

        Requires Pro plan.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: from_version
          in: query
          schema:
            type: string
        - name: to_version
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Visual diff metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  change_percentage:
                    type: number
                    nullable: true
                  threshold:
                    type: number
                  from_version:
                    type: string
                  to_version:
                    type: string
                  versions:
                    type: array
                    items:
                      type: string
        '400':
          description: Watch does not use screenshot monitoring
        '403':
          description: Pro plan required
  /watch/{uuid}/screenshot/history:
    get:
      operationId: getWatchScreenshotHistory
      tags:
        - Screenshots
      summary: Get screenshot history timestamps
      description: >-
        Returns the list of screenshot timestamps available for a watch with
        screenshot monitoring enabled.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
      responses:
        '200':
          description: Screenshot history timestamps
          content:
            application/json:
              schema:
                type: object
                properties:
                  timestamps:
                    type: array
                    items:
                      type: string
        '400':
          description: Watch does not have screenshot monitoring enabled
        '403':
          description: Pro plan required
        '404':
          description: Watch not found
  /watch/{uuid}/difference/{from_timestamp}/{to_timestamp}:
    get:
      operationId: getWatchHistoryDiff
      tags:
        - Watch History
      summary: Get the difference between two snapshots
      description: >
        Generate a difference (comparison) between two historical snapshots of a
        web page change monitor (watch).


        This endpoint compares content between two points in time and returns
        the differences in your chosen format.

        Perfect for reviewing what changed between specific versions or
        comparing recent changes.


        **Timestamp Keywords:**

        - Use `'latest'` for the most recent snapshot (to_timestamp)

        - Use `'previous'` for the second-most-recent snapshot (from_timestamp)

        - Or use specific Unix timestamps from the watch history


        **Format Options:**

        - `text` (default): Plain text with (removed) and (added) prefixes

        - `html`: HTML format with (removed) and (added) text

        - `htmlcolor`: Rich HTML with colored highlights (green for additions,
        red for deletions)


        **Word-Level Diffing:**

        - Enable word-level granularity with `word_diff=true` for detailed
        inline comparisons

        - Disable with `word_diff=false` for line-level comparisons only
        (default false/off, line-level mode by default)


        **Raw Diff Output:**

        - Use `no_markup=true` to get raw diff content without any formatting
        applied

        - Returns content with placeholders for opening/closing tags of changes

        - Allows you to implement your own custom colorisation or formatting

        - Skips all HTML color application and service tweaks (added text, html
        color tags, etc)
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
        - name: from_timestamp
          in: path
          required: true
          description: >-
            Starting snapshot timestamp, 'previous' for second-most-recent, or
            specific Unix timestamp
          schema:
            oneOf:
              - type: integer
                description: Unix timestamp of the starting snapshot
              - type: string
                enum:
                  - previous
                description: >-
                  Use 'previous' to automatically select the second-most-recent
                  snapshot
          example: previous
        - name: to_timestamp
          in: path
          required: true
          description: >-
            Ending snapshot timestamp, 'latest' for most recent, or specific
            Unix timestamp
          schema:
            oneOf:
              - type: integer
                description: Unix timestamp of the ending snapshot
              - type: string
                enum:
                  - latest
                description: Use 'latest' to automatically select the most recent snapshot
          example: latest
        - name: format
          in: query
          description: >
            Output format for the diff:

            - `text` (default): Plain text with (removed) and (added) prefixes

            - `html`: Basic HTML format

            - `htmlcolor`: Rich HTML with colored backgrounds (red for
            deletions, green for additions)

            - `markdown`: Markdown format with HTML rendering
          schema:
            type: string
            enum:
              - text
              - html
              - htmlcolor
              - markdown
            default: text
        - name: word_diff
          in: query
          description: >
            Enable word-level diffing for more granular comparisons.

            When enabled, changes are highlighted at the word level rather than
            line level.

            Default is false (line-level mode).

            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'false'
        - name: no_markup
          in: query
          description: >
            When set to true, returns the raw diff content without any markup
            formatting.

            The content will include placeholders for opening/closing tags of
            the changes,

            allowing you to implement your own custom colorisation or
            formatting.

            This skips all HTML color application and service tweaks.

            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'false'
        - name: type
          in: query
          description: >
            Diff granularity type:

            - `diffLines` (default): Line-level comparison, showing which lines
            changed

            - `diffWords`: Word-level comparison, showing which words changed
            within lines


            This parameter is an alternative to `word_diff` for better alignment
            with the UI.

            If both are specified, `type=diffWords` will enable word-level
            diffing.
          schema:
            type: string
            enum:
              - diffLines
              - diffWords
            default: diffLines
        - name: changesOnly
          in: query
          description: >
            When enabled, only show lines/content that changed (no surrounding
            context).

            When disabled, include unchanged lines for context around changes.

            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'true'
        - name: ignoreWhitespace
          in: query
          description: >
            When enabled, ignore whitespace-only changes (spaces, tabs,
            newlines).

            Useful for focusing on content changes and ignoring formatting
            differences.

            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'false'
        - name: removed
          in: query
          description: |
            Include removed/deleted content in the diff output.
            When disabled, content that was deleted will not appear in the diff.
            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'true'
        - name: added
          in: query
          description: |
            Include added/new content in the diff output.
            When disabled, content that was added will not appear in the diff.
            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'true'
        - name: replaced
          in: query
          description: >
            Include replaced/modified content in the diff output.

            When disabled, content that was modified (changed from one value to
            another) will not appear in the diff.

            Accepts: true, false, 1, 0, yes, no, on, off
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
              - '1'
              - '0'
              - 'yes'
              - 'no'
              - 'on'
              - 'off'
            default: 'true'
      responses:
        '200':
          description: Formatted diff between the two snapshots
          content:
            text/plain:
              schema:
                type: string
                description: Plain text diff with change markers
            text/html:
              schema:
                type: string
                description: HTML formatted diff with styling
        '400':
          description: Invalid format parameter or invalid request
        '404':
          description: Watch not found, timestamps not found, or insufficient history
      x-codeSamples:
        - lang: curl
          source: >
            # Compare previous snapshot to latest with colored HTML

            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/difference/previous/latest?format=htmlcolor"
            \
              -H "x-api-key: YOUR_API_KEY"

            # Compare two specific timestamps in plain text with word-level diff

            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/difference/1640995200/1640998800?format=text&word_diff=true"
            \
              -H "x-api-key: YOUR_API_KEY"

            # Show only additions (hide removed/replaced content), ignore
            whitespace

            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/difference/previous/latest?format=htmlcolor&removed=false&replaced=false&ignoreWhitespace=true"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: |
            import requests

            headers = {'x-api-key': 'YOUR_API_KEY'}
            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            # Compare previous to latest with colored HTML output
            response = requests.get(
                f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}/difference/previous/latest',
                headers=headers,
                params={'format': 'htmlcolor'}
            )
            print(response.text)

            # Compare specific timestamps with word-level diff
            from_ts = '1640995200'
            to_ts = '1640998800'
            response = requests.get(
                f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}/difference/{from_ts}/{to_ts}',
                headers=headers,
                params={'format': 'text', 'word_diff': 'true'}
            )
            print(response.text)

            # Show only additions, ignore whitespace and use word-level diff
            response = requests.get(
                f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}/difference/previous/latest',
                headers=headers,
                params={
                    'format': 'htmlcolor',
                    'type': 'diffWords',
                    'removed': 'false',
                    'replaced': 'false',
                    'ignoreWhitespace': 'true'
                }
            )
            print(response.text)
  /watch/{uuid}/favicon:
    get:
      operationId: getWatchFavicon
      tags:
        - Favicon
      summary: Get watch favicon
      description: >-
        Get the favicon for a web page change monitor (watch) as displayed in
        the watch overview list.
      parameters:
        - name: uuid
          in: path
          required: true
          description: Web page change monitor (watch) unique ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Favicon binary data
          content:
            image/*:
              schema:
                type: string
                format: binary
        '404':
          description: Favicon not found
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/favicon"
            \
              -H "x-api-key: YOUR_API_KEY" \
              --output favicon.ico
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'

            response =
            requests.get(f'https://detect.coolify.vkuprin.com/api/v1/watch/{uuid}/favicon',
            headers=headers)

            with open('favicon.ico', 'wb') as f:
                f.write(response.content)
  /tags:
    get:
      operationId: listTags
      tags:
        - Tag Management
      summary: List all tags
      description: Return list of available tags/groups
      responses:
        '200':
          description: List of tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  $ref: '#/components/schemas/Tag'
              example:
                550e8400-e29b-41d4-a716-446655440000:
                  uuid: 550e8400-e29b-41d4-a716-446655440000
                  title: Production Sites
                  notification_urls:
                    - mailto:admin@example.com
                  notification_muted: false
                330e8400-e29b-41d4-a716-446655440001:
                  uuid: 330e8400-e29b-41d4-a716-446655440001
                  title: News Sources
                  notification_urls:
                    - discord://webhook_id/webhook_token
                  notification_muted: false
      x-codeSamples:
        - lang: curl
          source: |
            curl -X GET "https://detect.coolify.vkuprin.com/api/v1/tags" \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            response =
            requests.get('https://detect.coolify.vkuprin.com/api/v1/tags',
            headers=headers)

            print(response.json())
  /tag:
    post:
      operationId: createTag
      tags:
        - Tag Management
      summary: Create tag
      description: Create a single tag/group
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTag'
            example:
              title: Important Sites
      responses:
        '201':
          description: Tag created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid
                    description: UUID of the created tag
        '400':
          description: Invalid or unsupported tag
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://detect.coolify.vkuprin.com/api/v1/tag" \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "title": "Important Sites"
              }'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            data = {'title': 'Important Sites'}

            response =
            requests.post('https://detect.coolify.vkuprin.com/api/v1/tag',
                                   headers=headers, json=data)
            print(response.json())
  /tag/{uuid}:
    get:
      operationId: getTag
      tags:
        - Tag Management
      summary: Get single tag
      description: >-
        Retrieve tag information, set notification_muted status, recheck all web
        page change monitors (watches) in tag.
      parameters:
        - name: uuid
          in: path
          required: true
          description: Tag unique ID
          schema:
            type: string
            format: uuid
        - name: muted
          in: query
          description: Set mute state
          schema:
            type: string
            enum:
              - muted
              - unmuted
        - name: recheck
          in: query
          description: >-
            Queue all web page change monitors (watches) with this tag for
            recheck
          schema:
            type: string
            enum:
              - 'true'
      responses:
        '200':
          description: Tag information or operation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tag'
            text/plain:
              schema:
                type: string
                example: OK
        '404':
          description: Tag not found
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/tag/550e8400-e29b-41d4-a716-446655440000"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            tag_uuid = '550e8400-e29b-41d4-a716-446655440000'

            response =
            requests.get(f'https://detect.coolify.vkuprin.com/api/v1/tag/{tag_uuid}',
            headers=headers)

            print(response.json())
    put:
      operationId: updateTag
      tags:
        - Tag Management
      summary: Update tag
      description: Update an existing tag using JSON
      parameters:
        - name: uuid
          in: path
          required: true
          description: Tag unique ID
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Tag'
      responses:
        '200':
          description: Tag updated successfully
        '500':
          description: Server error
      x-codeSamples:
        - lang: curl
          source: >
            curl -X PUT
            "https://detect.coolify.vkuprin.com/api/v1/tag/550e8400-e29b-41d4-a716-446655440000"
            \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "title": "Updated Production Sites",
                "notification_muted": false
              }'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            tag_uuid = '550e8400-e29b-41d4-a716-446655440000'

            data = {
                'title': 'Updated Production Sites',
                'notification_muted': False
            }

            response =
            requests.put(f'https://detect.coolify.vkuprin.com/api/v1/tag/{tag_uuid}',
                                  headers=headers, json=data)
            print(response.text)
    delete:
      operationId: deleteTag
      tags:
        - Tag Management
      summary: Delete tag
      description: >-
        Delete a tag/group and remove it from all web page change monitors
        (watches)
      parameters:
        - name: uuid
          in: path
          required: true
          description: Tag unique ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Tag deleted successfully
      x-codeSamples:
        - lang: curl
          source: >
            curl -X DELETE
            "https://detect.coolify.vkuprin.com/api/v1/tag/550e8400-e29b-41d4-a716-446655440000"
            \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            tag_uuid = '550e8400-e29b-41d4-a716-446655440000'

            response =
            requests.delete(f'https://detect.coolify.vkuprin.com/api/v1/tag/{tag_uuid}',
            headers=headers)

            print(response.text)
  /notifications:
    get:
      operationId: getNotifications
      tags:
        - Notifications
      summary: Get notification URLs
      description: Return the notification URL list from the configuration
      responses:
        '200':
          description: List of notification URLs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationUrls'
        '403':
          description: Feature not available - check feature flags
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/notifications" \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            response =
            requests.get('https://detect.coolify.vkuprin.com/api/v1/notifications',
            headers=headers)

            print(response.json())
    post:
      operationId: addNotifications
      tags:
        - Notifications
      summary: Add notification URLs
      description: Add one or more notification URLs to the configuration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationUrls'
            example:
              notification_urls:
                - mailto:admin@example.com
                - discord://webhook_id/webhook_token
      responses:
        '201':
          description: Notification URLs added successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationUrls'
        '400':
          description: Invalid input
        '403':
          description: Feature not available - check feature flags
      x-codeSamples:
        - lang: curl
          source: >
            curl -X POST
            "https://detect.coolify.vkuprin.com/api/v1/notifications" \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "notification_urls": [
                  "mailto:admin@example.com",
                  "discord://webhook_id/webhook_token"
                ]
              }'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            data = {
                'notification_urls': [
                    'mailto:admin@example.com',
                    'discord://webhook_id/webhook_token'
                ]
            }

            response =
            requests.post('https://detect.coolify.vkuprin.com/api/v1/notifications',
                                   headers=headers, json=data)
            print(response.json())
    put:
      operationId: replaceNotifications
      tags:
        - Notifications
      summary: Replace notification URLs
      description: Replace all notification URLs with the provided list (can be empty)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationUrls'
      responses:
        '200':
          description: Notification URLs replaced successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationUrls'
        '400':
          description: Invalid input
        '403':
          description: Feature not available - check feature flags
      x-codeSamples:
        - lang: curl
          source: >
            curl -X PUT
            "https://detect.coolify.vkuprin.com/api/v1/notifications" \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "notification_urls": [
                  "mailto:newadmin@example.com"
                ]
              }'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            data = {
                'notification_urls': [
                    'mailto:newadmin@example.com'
                ]
            }

            response =
            requests.put('https://detect.coolify.vkuprin.com/api/v1/notifications',
                                  headers=headers, json=data)
            print(response.json())
    delete:
      operationId: deleteNotifications
      tags:
        - Notifications
      summary: Delete notification URLs
      description: Delete one or more notification URLs from the configuration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationUrls'
      responses:
        '204':
          description: Notification URLs deleted successfully
        '400':
          description: No matching notification URLs found
        '403':
          description: Feature not available - check feature flags
      x-codeSamples:
        - lang: curl
          source: >
            curl -X DELETE
            "https://detect.coolify.vkuprin.com/api/v1/notifications" \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
                "notification_urls": [
                  "mailto:admin@example.com"
                ]
              }'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'application/json'
            }

            data = {
                'notification_urls': [
                    'mailto:admin@example.com'
                ]
            }

            response =
            requests.delete('https://detect.coolify.vkuprin.com/api/v1/notifications',
                                     headers=headers, json=data)
            print(response.status_code)
  /notifications/test:
    post:
      operationId: testNotificationUrl
      summary: Send a test notification
      description: >-
        Send a test notification to verify webhook URL(s) are configured
        correctly. Available on all plans.
      tags:
        - Notifications
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationUrls'
            example:
              notification_urls:
                - json://example.com/webhook
      responses:
        '200':
          description: Test notification sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              example:
                success: true
                message: Test notification sent
        '400':
          description: Invalid URL or failed to send
        '403':
          description: Feature not available - check feature flags
  /search:
    get:
      operationId: searchWatches
      tags:
        - Search
      summary: Search watches
      description: Search web page change monitors (watches) by URL or title text
      parameters:
        - name: q
          in: query
          required: true
          description: Search query to match against watch URLs and titles
          schema:
            type: string
        - name: tag
          in: query
          description: Tag name to limit results (name not UUID)
          schema:
            type: string
        - name: partial
          in: query
          description: Allow partial matching of URL query
          schema:
            type: string
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResult'
              example:
                watches:
                  095be615-a8ad-4c33-8e9c-c7612fbf6c9f:
                    uuid: 095be615-a8ad-4c33-8e9c-c7612fbf6c9f
                    url: http://example.com
                    title: Example Website Monitor
                    tags:
                      - 550e8400-e29b-41d4-a716-446655440000
                    paused: false
                    notification_muted: false
      x-codeSamples:
        - lang: curl
          source: >
            curl -X GET
            "https://detect.coolify.vkuprin.com/api/v1/search?q=example.com" \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            params = {'q': 'example.com'}

            response =
            requests.get('https://detect.coolify.vkuprin.com/api/v1/search',
                                  headers=headers, params=params)
            print(response.json())
  /import:
    post:
      operationId: importWatches
      tags:
        - Import
      summary: Import watch URLs with configuration
      description: >
        Import a list of URLs to monitor with optional watch configuration.
        Accepts line-separated URLs in request body.


        **Configuration via Query Parameters:**


        You can pass ANY watch configuration field as query parameters to apply
        settings to all imported watches.

        All parameters from the Watch schema are supported (processor,
        fetch_backend, notification_urls, etc.).


        **Special Parameters:**

        - `tag` / `tag_uuids` - Assign tags to imported watches

        - `proxy` - Use specific proxy for imported watches

        - `dedupe` - Skip duplicate URLs (default: true)


        **Type Conversion:**

        - Booleans: `true`, `false`, `1`, `0`, `yes`, `no`

        - Arrays: Comma-separated or JSON format (`[item1,item2]`)

        - Objects: JSON format (`{"key":"value"}`)

        - Numbers: Parsed as int or float
      parameters:
        - name: tag_uuids
          in: query
          description: >-
            Tag UUID(s) to apply to imported watches (comma-separated for
            multiple)
          schema:
            type: string
          example: 550e8400-e29b-41d4-a716-446655440000
        - name: tag
          in: query
          description: Tag name to apply to imported watches
          schema:
            type: string
          example: production
        - name: proxy
          in: query
          description: Proxy key to use for imported watches
          schema:
            type: string
          example: proxy1
        - name: dedupe
          in: query
          description: Skip duplicate URLs (default true)
          schema:
            type: boolean
            default: true
      requestBody:
        required: true
        content:
          text/plain:
            schema:
              type: string
            example: |
              https://example.com
              https://example.org
              https://example.net
      responses:
        '200':
          description: URLs imported successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
                  format: uuid
                description: List of created watch UUIDs
        '500':
          description: Server error
      x-codeSamples:
        - lang: curl
          source: >
            # Basic import

            curl -X POST "https://detect.coolify.vkuprin.com/api/v1/import" \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: text/plain" \
              -d $'https://example.com\nhttps://example.org\nhttps://example.net'

            # Import with processor and fetch backend

            curl -X POST
            "https://detect.coolify.vkuprin.com/api/v1/import?processor=restock_diff&fetch_backend=html_webdriver"
            \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: text/plain" \
              -d $'https://example.com\nhttps://example.org'

            # Import with multiple settings

            curl -X POST
            "https://detect.coolify.vkuprin.com/api/v1/import?processor=restock_diff&paused=true&tag=production"
            \
              -H "x-api-key: YOUR_API_KEY" \
              -H "Content-Type: text/plain" \
              -d $'https://example.com'
        - lang: Python
          source: >
            import requests


            headers = {
                'x-api-key': 'YOUR_API_KEY',
                'Content-Type': 'text/plain'
            }


            # Basic import

            urls =
            'https://example.com\nhttps://example.org\nhttps://example.net'

            response =
            requests.post('https://detect.coolify.vkuprin.com/api/v1/import',
                                   headers=headers, data=urls)
            print(response.json())


            # Import with configuration

            params = {
                'processor': 'restock_diff',
                'fetch_backend': 'html_webdriver',
                'paused': 'false',
                'tag': 'production'
            }

            response =
            requests.post('https://detect.coolify.vkuprin.com/api/v1/import',
                                   headers=headers, params=params, data=urls)
            print(response.json())
  /systeminfo:
    get:
      operationId: getSystemInfo
      tags:
        - System Information
      summary: Get system information
      description: Return information about the current system state
      responses:
        '200':
          description: System information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemInfo'
              example:
                watch_count: 42
                tag_count: 5
                uptime: 2 days, 3:45:12
                version: 0.50.10
      x-codeSamples:
        - lang: curl
          source: |
            curl -X GET "https://detect.coolify.vkuprin.com/api/v1/systeminfo" \
              -H "x-api-key: YOUR_API_KEY"
        - lang: Python
          source: >
            import requests


            headers = {'x-api-key': 'YOUR_API_KEY'}

            response =
            requests.get('https://detect.coolify.vkuprin.com/api/v1/systeminfo',
            headers=headers)

            print(response.json())
  /settings/user:
    get:
      operationId: getUserSettings
      summary: Get user-specific settings
      description: >-
        Retrieve the authenticated user's custom settings, including recheck
        interval preferences and email notification settings.
      tags:
        - User Settings
      responses:
        '200':
          description: User settings retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  time_between_check:
                    $ref: '#/components/schemas/TimeBetweenCheck'
                  email_notifications:
                    $ref: '#/components/schemas/EmailNotifications'
                  global_ignore_text:
                    type: array
                    items:
                      type: string
                      maxLength: 5000
                    maxItems: 100
                    description: >-
                      Text patterns to ignore in change detection across all
                      watches. Supports plain text and /regex/ patterns.
                  global_subtractive_selectors:
                    type: array
                    items:
                      type: string
                      maxLength: 5000
                    maxItems: 100
                    description: >-
                      CSS/XPath selectors to remove content from all watched
                      pages before comparison.
                  noise_reduction_timestamps:
                    type: boolean
                    description: >-
                      When enabled, automatically ignores timestamp-like text
                      changes (e.g. "3 minutes ago", "1.2k views") across all
                      watches.
                  noise_reduction_ads:
                    type: boolean
                    description: >-
                      When enabled, automatically removes ad elements, cookie
                      banners, and newsletter popups from all watched pages
                      using community-maintained filter lists (EasyList +
                      Fanboy's Cookie List).
                  ai_provider:
                    type: string
                    enum:
                      - anthropic
                      - openai
                      - google
                      - ''
                    description: >-
                      The user's selected AI provider for change summaries and
                      smart alerts.
                  ai_api_key_set:
                    type: boolean
                    description: Whether the user has an AI API key configured.
                  ai_api_key_masked:
                    type: string
                    description: Masked version of the AI API key for display.
                  anthropic_api_key_set:
                    type: boolean
                    description: >-
                      (Deprecated) Whether the user has an Anthropic API key
                      configured.
                  anthropic_api_key_masked:
                    type: string
                    description: (Deprecated) Masked version of the Anthropic API key.
              example:
                time_between_check:
                  hours: 6
                email_notifications:
                  enabled: true
                  email: user@example.com
                global_ignore_text: []
                global_subtractive_selectors: []
                noise_reduction_timestamps: true
                noise_reduction_ads: true
                ai_provider: anthropic
                ai_api_key_set: true
                ai_api_key_masked: sk-ant-t...abcd
        '401':
          description: Unauthorized - API key required
    put:
      operationId: updateUserSettings
      summary: Update user-specific settings
      description: >-
        Update the authenticated user's custom settings, such as default recheck
        interval for new watches, email notification preferences, and global
        ignore patterns.
      tags:
        - User Settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                time_between_check:
                  $ref: '#/components/schemas/TimeBetweenCheck'
                email_notifications:
                  $ref: '#/components/schemas/EmailNotifications'
                global_ignore_text:
                  type: array
                  items:
                    type: string
                    maxLength: 5000
                  maxItems: 100
                  description: >-
                    Text patterns to ignore in change detection across all
                    watches. Supports plain text and /regex/ patterns.
                global_subtractive_selectors:
                  type: array
                  items:
                    type: string
                    maxLength: 5000
                  maxItems: 100
                  description: >-
                    CSS/XPath selectors to remove content from all watched pages
                    before comparison.
                noise_reduction_timestamps:
                  type: boolean
                  description: >-
                    When enabled, automatically ignores timestamp-like text
                    changes across all watches.
                noise_reduction_ads:
                  type: boolean
                  description: >-
                    When enabled, automatically removes ad elements and cookie
                    banners using community-maintained filter lists.
                ai_provider:
                  type: string
                  enum:
                    - anthropic
                    - openai
                    - google
                    - ''
                  description: AI provider to use for change summaries and smart alerts.
                ai_api_key:
                  type: string
                  description: API key for the selected AI provider.
                anthropic_api_key:
                  type: string
                  description: >-
                    (Deprecated) Use ai_api_key with ai_provider=anthropic
                    instead.
            example:
              time_between_check:
                hours: 6
              email_notifications:
                enabled: true
                email: user@example.com
              global_ignore_text:
                - /\d{4}-\d{2}-\d{2}/
                - cookie
              global_subtractive_selectors:
                - .cookie-banner
                - nav
      responses:
        '200':
          description: Settings updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  time_between_check:
                    $ref: '#/components/schemas/TimeBetweenCheck'
                  email_notifications:
                    $ref: '#/components/schemas/EmailNotifications'
                  global_ignore_text:
                    type: array
                    items:
                      type: string
                    description: >-
                      Text patterns to ignore in change detection across all
                      watches.
                  global_subtractive_selectors:
                    type: array
                    items:
                      type: string
                    description: >-
                      CSS/XPath selectors to remove content from all watched
                      pages.
                  noise_reduction_timestamps:
                    type: boolean
                    description: Whether timestamp/counter noise reduction is enabled.
                  noise_reduction_ads:
                    type: boolean
                    description: Whether ad/cookie banner removal is enabled.
        '400':
          description: >-
            Invalid input - at least one time value must be greater than 0, or
            invalid email format
        '401':
          description: Unauthorized - API key required
  /settings/user/test-email:
    post:
      operationId: testEmailNotification
      summary: Send a test email notification
      description: >-
        Send a test email to verify email notification settings are configured
        correctly.
      tags:
        - User Settings
      responses:
        '200':
          description: Test email sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              example:
                success: true
                message: Test email sent successfully
        '400':
          description: Email notifications not configured or SMTP not configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              example:
                success: false
                message: Email notifications are not enabled
        '401':
          description: Unauthorized - API key required
  /email:
    get:
      operationId: getEmailStatus
      summary: Get email verification status
      description: Get the current email address and verification status for your API key.
      tags:
        - Email Verification
      responses:
        '200':
          description: Email status retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  email:
                    type: string
                    nullable: true
                    description: The email address, or null if not set
                  verified:
                    type: boolean
                    description: Whether the email is verified
                  muted:
                    type: boolean
                    description: Whether email notifications are paused
              example:
                email: user@example.com
                verified: true
                muted: false
        '400':
          description: Email settings not available for admin keys
        '401':
          description: Unauthorized - API key required
    post:
      operationId: setEmail
      summary: Set, update, or disable email address
      description: >
        Set or update the email address for your API key. A verification email
        will be sent

        to the provided address. The email must be verified before notifications
        can be sent.


        To disable email notifications, send an empty string for the email
        field.
      tags:
        - Email Verification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  description: >-
                    The email address to set, or empty string to disable
                    notifications
                muted:
                  type: boolean
                  description: >-
                    Pause or resume email notifications without removing the
                    email
            examples:
              setEmail:
                summary: Set or update email
                value:
                  email: user@example.com
              disableEmail:
                summary: Disable email notifications
                value:
                  email: ''
              muteEmail:
                summary: Pause email notifications
                value:
                  muted: true
              unmuteEmail:
                summary: Resume email notifications
                value:
                  muted: false
      responses:
        '200':
          description: Email updated or disabled successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  email:
                    type: string
                    nullable: true
                    description: The email address, or null if disabled
                  email_sent:
                    type: boolean
                    description: Whether the verification email was successfully sent
                  expires_in_hours:
                    type: integer
              examples:
                emailSet:
                  summary: Email verification sent
                  value:
                    message: Verification email sent
                    email: user@example.com
                    email_sent: true
                    expires_in_hours: 24
                emailDisabled:
                  summary: Email notifications disabled
                  value:
                    message: Email notifications disabled
                    email: null
                    email_sent: false
                emailMuted:
                  summary: Email notifications paused
                  value:
                    message: Email notifications muted
                    muted: true
        '400':
          description: Invalid email address or email settings not available for admin keys
        '401':
          description: Unauthorized - API key required
  /email/unsubscribe:
    get:
      operationId: unsubscribeEmail
      summary: Unsubscribe from email notifications (no auth)
      description: >-
        Single-click unsubscribe using a token. Returns an HTML confirmation
        page.
      tags:
        - Email Verification
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: HTML confirmation page
          content:
            text/html:
              schema:
                type: string
        '400':
          description: Invalid token
  /email/enable:
    get:
      operationId: enableEmail
      summary: Enable email notifications (no auth)
      description: Single-click enable using a token. Returns an HTML confirmation page.
      tags:
        - Email Verification
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: HTML confirmation page
          content:
            text/html:
              schema:
                type: string
        '400':
          description: Invalid token
  /email/verify:
    get:
      operationId: verifyEmail
      summary: Verify email address
      description: >
        Verify an email address using the token from the verification email.

        This endpoint does not require authentication as the token serves as
        proof.
      tags:
        - Email Verification
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: The verification token from the email
      responses:
        '200':
          description: Email verified successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  email:
                    type: string
              example:
                message: Email verified successfully
                email: user@example.com
        '400':
          description: Invalid or expired token
  /email/resend:
    post:
      operationId: resendVerificationEmail
      summary: Resend verification email
      description: >-
        Resend the verification email for the email address associated with your
        API key.
      tags:
        - Email Verification
      responses:
        '200':
          description: Verification email sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  email:
                    type: string
                  expires_in_hours:
                    type: integer
              example:
                message: Verification email sent
                email: user@example.com
                expires_in_hours: 24
        '400':
          description: >-
            No email set, email already verified, or email settings not
            available for admin keys
        '401':
          description: Unauthorized - API key required
  /telegram:
    get:
      operationId: getTelegramStatus
      summary: Get Telegram connection status
      description: >-
        Get the current Telegram connection status for your API key, including
        any pending link.
      tags:
        - Telegram Notifications
      responses:
        '200':
          description: Telegram status retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  connected:
                    type: boolean
                  chat_id:
                    type: string
                  pending_link:
                    type: string
                    nullable: true
                    description: Telegram deep-link URL if connection is pending
                  bot_configured:
                    type: boolean
                    description: Whether the Telegram bot is configured on the server
              example:
                connected: false
                chat_id: ''
                pending_link: https://t.me/SiteSpyBot?start=abc123
                bot_configured: true
        '400':
          description: Telegram settings not available for admin keys
        '401':
          description: Unauthorized - API key required
        '404':
          description: API key not found
    post:
      operationId: setTelegram
      summary: Generate Telegram connect link
      description: >
        Generate a deep-link URL for connecting your Telegram account.

        Open the returned link in Telegram and tap Start to complete the
        connection.
      tags:
        - Telegram Notifications
      responses:
        '200':
          description: Connect link generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  link_url:
                    type: string
                    description: Telegram deep-link URL to open
                  expires_in:
                    type: integer
                    description: Link expiry time in seconds
              example:
                link_url: https://t.me/SiteSpyBot?start=abc123def456
                expires_in: 1800
        '400':
          description: Already connected or invalid request
        '401':
          description: Unauthorized - API key required
        '404':
          description: API key not found
        '500':
          description: Telegram bot not configured on server
    delete:
      operationId: deleteTelegram
      summary: Remove Telegram notifications
      description: Disconnect Telegram and stop receiving notifications.
      tags:
        - Telegram Notifications
      responses:
        '200':
          description: Telegram notifications disabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
              example:
                message: Telegram notifications disabled
        '401':
          description: Unauthorized - API key required
        '404':
          description: API key not found
  /telegram/test:
    post:
      operationId: testTelegram
      summary: Send a test Telegram message
      description: Send a test notification to verify Telegram is configured correctly.
      tags:
        - Telegram Notifications
      responses:
        '200':
          description: Test message sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              example:
                success: true
                message: Test message sent
        '400':
          description: Telegram not configured or failed to send
        '401':
          description: Unauthorized - API key required
        '500':
          description: Telegram bot not configured on server
  /telegram/webhook:
    post:
      operationId: telegramWebhook
      summary: Telegram webhook endpoint
      description: >
        Receives updates from the Telegram Bot API. This endpoint is called by
        Telegram

        when users interact with the bot (e.g. /start command). Not intended for
        direct use.
      tags:
        - Telegram Notifications
      security: []
      responses:
        '200':
          description: Update processed
        '403':
          description: Invalid webhook secret
  /webpush/vapid-key:
    get:
      operationId: getWebPushVapidKey
      summary: Get the public VAPID key
      description: >-
        Returns the server's public VAPID key needed to subscribe to push
        notifications.
      tags:
        - Web Push Notifications
      responses:
        '200':
          description: VAPID key returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  public_key:
                    type: string
                    description: Base64url-encoded public VAPID key
        '503':
          description: Web push not configured on this server
  /webpush/subscription:
    get:
      operationId: getWebPushStatus
      summary: Get web push subscription status
      description: Check whether the current API key has any active push subscriptions.
      tags:
        - Web Push Notifications
      responses:
        '200':
          description: Subscription status
          content:
            application/json:
              schema:
                type: object
                properties:
                  connected:
                    type: boolean
                  subscription_count:
                    type: integer
    post:
      operationId: subscribeWebPush
      summary: Save a push subscription
      description: Store a browser push subscription for the current API key.
      tags:
        - Web Push Notifications
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - subscription
              properties:
                subscription:
                  type: object
                  required:
                    - endpoint
                    - keys
                  properties:
                    endpoint:
                      type: string
                    keys:
                      type: object
                      required:
                        - p256dh
                        - auth
                      properties:
                        p256dh:
                          type: string
                        auth:
                          type: string
      responses:
        '201':
          description: Subscription saved
        '400':
          description: Invalid subscription data
        '404':
          description: API key not found
    delete:
      operationId: unsubscribeWebPush
      summary: Remove push subscription(s)
      description: >
        Remove a specific push subscription by endpoint, or remove all
        subscriptions

        if no endpoint is provided.
      tags:
        - Web Push Notifications
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                endpoint:
                  type: string
                  description: >-
                    Specific subscription endpoint to remove. Omit to remove
                    all.
      responses:
        '200':
          description: Subscription(s) removed
        '404':
          description: API key not found
  /webpush/test:
    post:
      operationId: testWebPush
      summary: Send a test push notification
      description: Send a test push notification to all subscribed browsers.
      tags:
        - Web Push Notifications
      responses:
        '200':
          description: Test notification sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: No subscriptions or failed to send
        '401':
          description: Unauthorized - API key required
        '503':
          description: Web push not configured on server
  /generate_key:
    post:
      operationId: generateApiKey
      summary: Generate a new API key
      description: >
        Generate a new unique API key for accessing the API. This endpoint does
        not require

        authentication. Each API key is associated with a unique user account.


        **Rate Limiting**: This endpoint is rate-limited to prevent abuse.
        Excessive requests

        from the same IP address will result in a 429 Too Many Requests
        response.
      tags:
        - API Key Management
      security: []
      responses:
        '200':
          description: API key generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key:
                    type: string
                    description: The newly generated API key
                required:
                  - api_key
              example:
                api_key: abc123def456ghi789jkl012mno345pqr
        '429':
          description: Too many requests - rate limit exceeded
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
        '500':
          description: Server error generating API key
  /feature-flags:
    get:
      operationId: getFeatureFlags
      summary: Get feature flags
      description: >
        Retrieve the feature flags for the current API key. Feature flags
        control which

        features are enabled or disabled for specific users.
      tags:
        - Feature Flags
      responses:
        '200':
          description: Feature flags retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  feature_flags:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        enabled:
                          type: boolean
                        name:
                          type: string
                        description:
                          type: string
              example:
                feature_flags:
                  email_notifications:
                    enabled: true
                    name: Email Notifications
                    description: Enable email notifications for watch changes
        '401':
          description: Unauthorized - API key required
  /auth/altcha-challenge:
    get:
      tags:
        - Authentication
      summary: Create an ALTCHA signup challenge
      description: |
        Create a signed proof-of-work challenge for the signup form. The solved
        payload must be submitted as the `altcha` field when registering.
      security: []
      responses:
        '200':
          description: ALTCHA challenge created
          content:
            application/json:
              schema:
                type: object
                required:
                  - parameters
                  - signature
                properties:
                  parameters:
                    type: object
                    required:
                      - algorithm
                      - cost
                      - keyLength
                      - keyPrefix
                      - nonce
                      - salt
                    properties:
                      algorithm:
                        type: string
                        example: SHA-256
                      cost:
                        type: integer
                        example: 1
                      keyLength:
                        type: integer
                        example: 32
                      keyPrefix:
                        type: string
                        example: '0000'
                      nonce:
                        type: string
                        example: 0123456789abcdef0123456789abcdef
                      salt:
                        type: string
                        example: fedcba9876543210fedcba9876543210
                      expiresAt:
                        type: integer
                        description: Unix timestamp when the challenge expires
                        example: 1778203946
                  signature:
                    type: string
                    example: 8b9c...
  /auth/register:
    post:
      tags:
        - Authentication
      summary: Register a new user account
      description: >
        Create a new user account with email and password. An API key is
        automatically

        generated and linked to the account. This API key can be used for all
        subsequent

        authenticated requests. Signup requires a solved ALTCHA proof-of-work
        payload

        from `/auth/altcha-challenge`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
                - altcha
              properties:
                email:
                  type: string
                  format: email
                  description: User's email address
                  example: user@example.com
                password:
                  type: string
                  minLength: 8
                  description: Password (minimum 8 characters)
                  example: secretpassword123
                name:
                  type: string
                  description: User's display name (optional)
                  example: John Doe
                altcha:
                  type: string
                  description: Base64 ALTCHA verification payload
                  example: eyJjaGFsbGVuZ2UiOiB7...
      responses:
        '200':
          description: Account created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Account created successfully
                  user:
                    type: object
                    properties:
                      email:
                        type: string
                        example: user@example.com
                      name:
                        type: string
                        example: John Doe
                      api_key:
                        type: string
                        example: abc123def456...
                      email_verified:
                        type: boolean
                        description: Whether the user's email has been verified
                        example: false
        '400':
          description: Invalid input (missing fields, invalid email, or disposable email)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Email and password are required
        '409':
          description: User already exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: User with this email already exists
        '429':
          description: Too many signup attempts from this IP
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Too many signup attempts. Please try again later.
  /auth/login:
    post:
      tags:
        - Authentication
      summary: Login to existing account
      description: |
        Authenticate with email and password to retrieve your API key.
        Use this API key for all subsequent authenticated requests.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                  description: User's email address
                  example: user@example.com
                password:
                  type: string
                  description: User's password
                  example: secretpassword123
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Login successful
                  user:
                    type: object
                    properties:
                      email:
                        type: string
                        example: user@example.com
                      name:
                        type: string
                        example: John Doe
                      api_key:
                        type: string
                        example: abc123def456...
                      email_verified:
                        type: boolean
                        description: Whether the user's email has been verified
                        example: true
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Email and password are required
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Invalid credentials
  /auth/social-login:
    post:
      tags:
        - Authentication
      summary: Login or register via social/OAuth provider
      description: >
        Authenticate a user via a social login provider (e.g., Google).

        If the user already exists, returns their API key.

        If the user doesn't exist, auto-creates an account and returns the new
        API key.

        This endpoint is intended to be called server-side from the NextAuth
        callback.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - provider
              properties:
                email:
                  type: string
                  format: email
                  description: User's email address from the OAuth provider
                  example: user@example.com
                name:
                  type: string
                  description: User's display name from the OAuth provider
                  example: John Doe
                provider:
                  type: string
                  enum:
                    - google
                  description: The OAuth provider name
                  example: google
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Login successful
                  user:
                    type: object
                    properties:
                      email:
                        type: string
                        example: user@example.com
                      name:
                        type: string
                        example: John Doe
                      api_key:
                        type: string
                        example: abc123def456...
                      email_verified:
                        type: boolean
                        description: Whether the user's email has been verified
                        example: true
        '400':
          description: Invalid input or unsupported provider
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Email is required
  /auth/me:
    get:
      tags:
        - Authentication
      summary: Get current user info
      description: |
        Retrieve information about the currently authenticated user.
        Requires a valid API key in the x-api-key header.
      responses:
        '200':
          description: User information
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      email:
                        type: string
                        example: user@example.com
                      name:
                        type: string
                        example: John Doe
                      api_key:
                        type: string
                        example: abc123def456...
                      created_at:
                        type: integer
                        description: Unix timestamp of account creation
                        example: 1704067200
                      email_verified:
                        type: boolean
                        description: Whether the user's email has been verified
                        example: true
        '401':
          description: Unauthorized - API key required
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key required
        '404':
          description: User not found for this API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: User not found
  /auth/verify-email:
    get:
      tags:
        - Authentication
      summary: Verify account email
      description: |
        Verify a user's account email using the token sent via email.
        Returns an HTML page with the verification result.
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Verification token from the email link
      responses:
        '200':
          description: Email verified successfully (HTML response)
          content:
            text/html:
              schema:
                type: string
        '400':
          description: Invalid or expired token (HTML response)
          content:
            text/html:
              schema:
                type: string
  /auth/resend-verification:
    post:
      tags:
        - Authentication
      summary: Resend account verification email
      description: |
        Resend the account verification email. Requires authentication.
        Rate limited to one request per 60 seconds.
      responses:
        '200':
          description: Verification email sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Verification email sent
                  expires_in_hours:
                    type: integer
                    example: 24
        '400':
          description: Email already verified or no email set
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Email already verified
        '401':
          description: API key required
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key is required
        '429':
          description: Rate limit - wait before requesting again
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Please wait before requesting another verification email
  /auth/forgot-password:
    post:
      tags:
        - Authentication
      summary: Request a password reset
      description: |
        Request a password reset token for the given email address.
        Always returns success to avoid leaking whether an email is registered.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Email address of the account to reset
                  example: user@example.com
      responses:
        '200':
          description: Reset request processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: >-
                      If an account with that email exists, a password reset
                      link has been generated.
                  reset_token:
                    type: string
                    nullable: true
                    description: >-
                      Reset token (temporary — will be removed when email
                      sending is implemented)
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Email is required
  /auth/reset-password:
    post:
      tags:
        - Authentication
      summary: Reset password with token
      description: >
        Reset the user's password using a valid reset token obtained from the
        forgot-password endpoint.

        Tokens expire after 1 hour.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - new_password
              properties:
                token:
                  type: string
                  description: Password reset token
                new_password:
                  type: string
                  minLength: 8
                  description: New password (minimum 8 characters)
                  example: newSecurePassword123
      responses:
        '200':
          description: Password reset successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Password has been reset successfully
        '400':
          description: Invalid or expired token, or invalid password
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Invalid or expired reset token
  /auth/change-password:
    post:
      tags:
        - Authentication
      summary: Change password for authenticated user
      description: |
        Change the password for the currently authenticated user.
        Requires the current password for verification.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - current_password
                - new_password
              properties:
                current_password:
                  type: string
                  description: Current password for verification
                new_password:
                  type: string
                  minLength: 8
                  description: New password (minimum 8 characters)
                  example: newSecurePassword123
      responses:
        '200':
          description: Password changed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Password changed successfully
        '400':
          description: Invalid input or incorrect current password
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Current password is incorrect
        '401':
          description: Unauthorized - API key required
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key is required
        '404':
          description: User not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: User not found
  /auth/regenerate-key:
    post:
      tags:
        - Authentication
      summary: Regenerate API key for authenticated user
      description: |
        Regenerate the API key for the currently authenticated user.
        The old key is invalidated and all watches are migrated to the new key.
        Only works for API keys linked to a user account.
      responses:
        '200':
          description: API key regenerated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: API key regenerated successfully
                  user:
                    type: object
                    properties:
                      email:
                        type: string
                        format: email
                        example: user@example.com
                      name:
                        type: string
                        nullable: true
                        example: John Doe
                      api_key:
                        type: string
                        example: a1b2c3d4e5f6...
        '400':
          description: API key is not linked to a user account
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key is not linked to a user account
        '401':
          description: Unauthorized - API key required or invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key is required
  /auth/delete-account:
    delete:
      tags:
        - Authentication
      summary: Permanently delete user account
      description: >
        Permanently deletes the authenticated user's account and all associated
        data,

        including all watches, settings, notification configurations, and
        password reset tokens.

        This action cannot be undone.
      operationId: deleteAccount
      responses:
        '200':
          description: Account deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Account deleted successfully
        '401':
          description: Unauthorized - API key required or invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key is required
        '403':
          description: API key is not linked to a user account
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: API key is not linked to a user account
  /plan:
    get:
      tags:
        - Plan
      summary: Get current plan and usage
      description: >-
        Returns the subscription plan, limits, and watch usage for the
        authenticated API key.
      operationId: getPlan
      responses:
        '200':
          description: Plan and usage information
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    type: string
                    enum:
                      - free
                      - starter
                      - pro
                      - business
                      - pro_trial
                    example: free
                  limits:
                    type: object
                    properties:
                      max_watches:
                        type: integer
                        example: 5
                      history_retention_days:
                        type: integer
                        description: >-
                          Number of days snapshots are retained before automatic
                          cleanup
                        example: 30
                      max_snapshots_per_watch:
                        type: integer
                        description: >-
                          Maximum number of snapshots kept per watch before
                          oldest are pruned
                        example: 5
                      min_check_interval_seconds:
                        type: integer
                        description: >-
                          Minimum allowed recheck interval in seconds for this
                          plan
                        example: 21600
                      default_check_interval_seconds:
                        type: integer
                        description: >-
                          Default recheck interval in seconds for watches that
                          use plan or user defaults
                        example: 3600
                      max_fast_watches:
                        type: integer
                        description: >-
                          Maximum active watches allowed below the plan default
                          interval
                        example: 0
                      max_check_interval_seconds:
                        type: integer
                        description: >-
                          Maximum allowed recheck interval in seconds for this
                          plan
                        example: 604800
                      label:
                        type: string
                        example: Free
                      price_monthly:
                        type: integer
                        example: 0
                  usage:
                    type: object
                    properties:
                      watches:
                        type: integer
                        example: 3
                      fast_watches:
                        type: integer
                        description: >-
                          Number of active watches configured below the plan
                          default interval
                        example: 0
                  billing:
                    type: object
                    properties:
                      subscription_status:
                        type: string
                        nullable: true
                        example: active
                      ls_subscription_id:
                        type: string
                        nullable: true
                      ls_customer_portal_url:
                        type: string
                        nullable: true
                      current_period_end:
                        type: string
                        nullable: true
                  trial:
                    type: object
                    nullable: true
                    properties:
                      trial_ends_at:
                        type: number
                        nullable: true
                        description: Unix timestamp when trial expires
                      trial_active:
                        type: boolean
                        description: Whether the trial is currently active
                      days_remaining:
                        type: integer
                        description: Days remaining in the trial (0 if expired)
                  email_verified:
                    type: boolean
                    description: Whether the account email has been verified
                  created_at:
                    type: integer
                    nullable: true
                    description: >-
                      Unix timestamp of account creation (null for guest API
                      keys)
                  pmf_submitted:
                    type: boolean
                    description: Whether the user has already submitted the PMF survey
                  has_detected_change:
                    type: boolean
                    description: >-
                      Whether at least one of the user's watches has fired a
                      change notification
        '403':
          description: Unauthorized
  /feedback/pmf:
    post:
      tags:
        - Feedback
      summary: Submit the PMF (Sean Ellis) survey
      description: >
        Records a one-time Product-Market-Fit survey response for the
        authenticated user.

        Returns 409 if the user has already submitted.
      operationId: submitPmfFeedback
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - pmf_score
                - answer
              properties:
                pmf_score:
                  type: string
                  enum:
                    - very_disappointed
                    - somewhat_disappointed
                    - not_disappointed
                    - no_longer_using
                answer:
                  type: string
                  minLength: 10
                  maxLength: 2000
                  description: Free-text response to the follow-up question
                segment:
                  type: string
                  nullable: true
                  enum:
                    - personal
                    - freelance
                    - small_business
                    - other
                  description: Self-reported use case (optional)
                price_reaction:
                  type: string
                  nullable: true
                  enum:
                    - would_pay
                    - find_alternative
                    - stop_using
                    - not_sure
                  description: >-
                    Reaction to €8/month price point (omit or null when
                    pmf_score is no_longer_using)
      responses:
        '200':
          description: Feedback recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: ok
        '400':
          description: Validation error
        '401':
          description: API key required
        '409':
          description: Feedback already submitted for this user
  /billing/checkout:
    post:
      tags:
        - Billing
      summary: Create a Lemon Squeezy checkout session
      description: Creates a hosted checkout URL for upgrading to a paid plan.
      operationId: createBillingCheckout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan
              properties:
                plan:
                  type: string
                  enum:
                    - starter
                    - pro
                    - business
                  example: starter
                billing_period:
                  type: string
                  enum:
                    - monthly
                    - yearly
                  default: monthly
                success_url:
                  type: string
                  description: URL to redirect to after successful checkout
                  example: https://sitespy.app/dashboard/billing?checkout=success
      responses:
        '200':
          description: Checkout URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  checkout_url:
                    type: string
                    example: https://checkout.lemonsqueezy.com/...
        '400':
          description: Invalid plan
        '503':
          description: Billing not configured on server
  /billing/webhook:
    post:
      tags:
        - Billing
      summary: Lemon Squeezy webhook receiver
      description: >-
        Receives subscription lifecycle events from Lemon Squeezy. Called by
        Lemon Squeezy, not clients.
      operationId: billingWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Webhook processed
        '401':
          description: Invalid signature
  /billing/portal:
    get:
      tags:
        - Billing
      summary: Get customer billing portal URL
      description: >-
        Returns the Lemon Squeezy customer portal URL for managing the
        subscription.
      operationId: getBillingPortal
      responses:
        '200':
          description: Portal URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  portal_url:
                    type: string
        '404':
          description: No active subscription
  /billing/status:
    get:
      tags:
        - Billing
      summary: Get billing subscription status
      description: Returns the current subscription status and metadata.
      operationId: getBillingStatus
      responses:
        '200':
          description: Billing status
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    type: string
                    enum:
                      - free
                      - starter
                      - pro
                  subscription_status:
                    type: string
                    nullable: true
                  ls_subscription_id:
                    type: string
                    nullable: true
                  current_period_end:
                    type: string
                    nullable: true
  /rss:
    get:
      operationId: getRssFeedSettings
      summary: Get RSS feed settings
      description: >
        Get the current user's RSS feed token and status. Use the returned token
        to construct

        RSS feed URLs for use in any RSS reader.
      tags:
        - RSS Feeds
      responses:
        '200':
          description: RSS feed settings retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                    description: Whether RSS feeds are enabled (token exists)
                  rss_token:
                    type: string
                    nullable: true
                    description: The RSS feed access token
              example:
                enabled: true
                rss_token: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
        '400':
          description: RSS feed settings not available for admin keys
        '401':
          description: Unauthorized - API key required
    post:
      operationId: generateRssToken
      summary: Generate RSS feed token
      description: >
        Generate (or regenerate) a personal RSS feed token. This token is used
        in RSS feed URLs

        to authenticate access. Only watches owned by this user will appear in
        the feeds.


        Feed URLs use the format: `{server}/rss?token={rss_token}`
      tags:
        - RSS Feeds
      responses:
        '200':
          description: RSS token generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                  rss_token:
                    type: string
                    description: The newly generated RSS feed token
              example:
                enabled: true
                rss_token: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
        '400':
          description: RSS feed settings not available for admin keys
        '401':
          description: Unauthorized - API key required
        '404':
          description: API key not found
    delete:
      operationId: revokeRssToken
      summary: Revoke RSS feed token
      description: Revoke the RSS feed token, disabling all RSS feed access for this user.
      tags:
        - RSS Feeds
      responses:
        '200':
          description: RSS token revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
              example:
                message: RSS feed token revoked
        '400':
          description: RSS feed settings not available for admin keys
        '401':
          description: Unauthorized - API key required
        '404':
          description: API key not found
