{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/rendex-dev/json-schema/rendex-dev-create-watch-request-schema.json",
  "title": "CreateWatchRequest",
  "description": "Validated with a STRICT schema — any field outside the list below is REJECTED with a\n400 VALIDATION_ERROR, not silently ignored. Render knobs go under `renderParams` (also strict).\n",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/rendex-dev-openapi.yaml#/components/schemas/CreateWatchRequest",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "url"
  ],
  "properties": {
    "url": {
      "type": "string",
      "format": "uri",
      "description": "The page to monitor"
    },
    "name": {
      "type": "string",
      "maxLength": 120,
      "description": "Optional label"
    },
    "intervalMinutes": {
      "type": "integer",
      "minimum": 5,
      "maximum": 43200,
      "default": 1440,
      "description": "Check frequency in minutes. The accepted minimum is your plan's floor — Free 1440\n(daily), Starter 180 (3h), Pro 30, Enterprise 5. A faster value returns 403\nWATCH_INTERVAL_TOO_FAST.\n"
    },
    "diffMode": {
      "type": "string",
      "enum": [
        "visual",
        "text",
        "both"
      ],
      "default": "both",
      "description": "How changes are detected. Default `both` = a pixel diff (with a highlighted overlay) AND a full-page text diff, alerting on either — catches any change, visual or text. `visual` or `text` narrow to a single signal. (`text`/`both` extract the full visible page text, not just the article body.)"
    },
    "threshold": {
      "type": "number",
      "minimum": 0,
      "maximum": 1,
      "default": 0.01,
      "description": "Visual sensitivity (0..1). Low (default 0.01) alerts on ANY change, including a small one on a long page (a changed-region test, not a whole-page pixel ratio). `0.06`+ = only MAJOR visual changes (whole-page ratio). Does not affect text detection."
    },
    "renderParams": {
      "$ref": "#/$defs/WatchRenderParams"
    },
    "aiSummary": {
      "type": "boolean",
      "default": false,
      "description": "Pro+ — opt in to an AI-written one-sentence \"what changed\" summary on each detected change. Off by default. Every change already gets a free templated summary regardless of this flag; enabling it upgrades a text change's summary to a Workers-AI sentence."
    },
    "webhookUrl": {
      "type": "string",
      "format": "uri",
      "description": "Starter+ — HMAC-signed change-webhook target. Delivered payload: the WatchWebhookEvent schema."
    },
    "notifyEmail": {
      "type": "string",
      "format": "email",
      "description": "Any plan — send change alerts here (defaults to your account email if omitted). Anti-abuse: must be your OWN account email; any other address is rejected with 400 VALIDATION_ERROR."
    },
    "paused": {
      "type": "boolean",
      "default": false,
      "description": "Create the watch paused (no baseline capture or charge until resumed). Paused watches still count toward your plan's watch limit."
    }
  },
  "$defs": {
    "WatchRenderParams": {
      "type": "object",
      "additionalProperties": false,
      "description": "Render knobs applied on every check — a subset of the screenshot capture params, stored on\nthe watch and fed to the renderer each run. Inline source params (`url`/`html`/`markdown`/\n`data`) and credential knobs (`cookies`/`headers`/`css`/`js`) are NOT accepted here; the\nwatch's target is the top-level `url`. Validated with a STRICT schema: any capture param\noutside the list below (e.g. `quality`, `css`, `js`, `resize*`, `pdf*`, `userAgent`,\n`cookies`, `headers`) is REJECTED with a 400 VALIDATION_ERROR — extras are NOT silently ignored.\n",
      "properties": {
        "format": {
          "type": "string",
          "enum": [
            "png",
            "jpeg",
            "webp",
            "pdf"
          ],
          "default": "png",
          "description": "A pdf cannot be visually diffed — pair it with diffMode `text`."
        },
        "width": {
          "type": "integer",
          "minimum": 320,
          "maximum": 3840,
          "default": 1280
        },
        "height": {
          "type": "integer",
          "minimum": 240,
          "maximum": 2160,
          "default": 800
        },
        "fullPage": {
          "type": "boolean",
          "default": true,
          "description": "Defaults to true for watches (overrides the screenshot default of false) so the whole\npage is monitored — a viewport-only capture would miss changes below the fold. Set false\n(or use a selector) to scope down on purpose.\n"
        },
        "device": {
          "type": "string",
          "enum": [
            "desktop",
            "iphone_15",
            "iphone_se",
            "pixel_8",
            "ipad",
            "ipad_pro"
          ]
        },
        "deviceScaleFactor": {
          "type": "number",
          "minimum": 1,
          "maximum": 3,
          "default": 2
        },
        "darkMode": {
          "type": "boolean",
          "default": false
        },
        "blockAds": {
          "type": "boolean",
          "default": true
        },
        "blockCookieBanners": {
          "type": "boolean",
          "default": false
        },
        "hideSelectors": {
          "type": "array",
          "maxItems": 50,
          "items": {
            "type": "string",
            "maxLength": 500
          },
          "description": "Remove these elements before capture"
        },
        "waitUntil": {
          "type": "string",
          "enum": [
            "load",
            "domcontentloaded",
            "networkidle0",
            "networkidle2"
          ],
          "default": "networkidle2"
        },
        "delay": {
          "type": "integer",
          "minimum": 0,
          "maximum": 10000,
          "default": 0,
          "description": "Extra wait in milliseconds after the page is ready"
        },
        "timeout": {
          "type": "integer",
          "minimum": 5,
          "maximum": 60,
          "default": 30,
          "description": "Navigation timeout in seconds"
        },
        "geo": {
          "type": "string",
          "minLength": 2,
          "maxLength": 2,
          "description": "Pro+ — 2-letter ISO 3166-1 alpha-2 country code (e.g. `US`, `GB`; case-insensitive, upper-cased server-side) to render from. Cannot be combined with a text diffMode."
        },
        "geoCity": {
          "type": "string"
        },
        "geoState": {
          "type": "string"
        },
        "selector": {
          "type": "string",
          "maxLength": 500,
          "description": "Capture and diff ONLY this CSS element — watch just one price, banner, or section\ninstead of the whole page. Visual-first (in text mode the extracted text is full-page).\n"
        },
        "ignoreRegions": {
          "type": "array",
          "maxItems": 20,
          "description": "Pixel rectangles masked in BOTH captures before the visual diff — silence dynamic zones\n(an ad slot, a clock) so they never trip a change. Visual mode only.\n",
          "items": {
            "type": "object",
            "required": [
              "x",
              "y",
              "width",
              "height"
            ],
            "properties": {
              "x": {
                "type": "number",
                "minimum": 0
              },
              "y": {
                "type": "number",
                "minimum": 0
              },
              "width": {
                "type": "number",
                "exclusiveMinimum": 0,
                "description": "Pixels (> 0)"
              },
              "height": {
                "type": "number",
                "exclusiveMinimum": 0,
                "description": "Pixels (> 0)"
              }
            }
          }
        },
        "ignoreText": {
          "type": "array",
          "maxItems": 50,
          "items": {
            "type": "string",
            "maxLength": 500
          },
          "description": "Substrings or `/regex/flags` patterns stripped from BOTH texts before the text diff —\nsilence boilerplate (timestamps, view counts, session tokens). Applies to text/both modes.\n"
        },
        "minTextChars": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100000,
          "description": "Minimum added + removed characters for a text change to count — ignore tiny edits below\nthe size you care about.\n"
        },
        "suppressWhilePresent": {
          "type": "array",
          "maxItems": 20,
          "items": {
            "type": "string",
            "maxLength": 500
          },
          "description": "While the page text contains any of these markers (e.g. \"Out of stock\", \"Loading\"), the\nrun is treated as unchanged — no alert, no baseline drift — until the marker clears.\n"
        },
        "uaMode": {
          "type": "string",
          "enum": [
            "auto",
            "identify",
            "stealth"
          ],
          "default": "auto",
          "description": "How the monitor presents its User-Agent. `auto` (default) identifies as RendexWatch and\nretries once with a standard browser identity if the page looks blocked; `identify`\nalways identifies as RendexWatch and never falls back; `stealth` always presents a\nstandard browser identity. See https://rendex.dev/bot.\n"
        }
      }
    }
  }
}