{
  "openapi": "3.0.1",
  "info": {
    "title": "Web Watchdog - Website Change Monitor & Diff API for AI Agents",
    "description": "Check if any URL changed since a given content hash, or watch it on a schedule. Diffs web pages, JSON APIs, RSS feeds and sitemaps and returns what changed, old vs new values, numeric deltas and a one-line summary. Billed per change detected, not per check. Strongest on APIs, docs and changelogs.",
    "version": "0.0",
    "x-build-id": "LOkjhuoLqmekBV4Wx"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/santhej~web-watchdog/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-santhej-web-watchdog",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/santhej~web-watchdog/runs": {
      "post": {
        "operationId": "runs-sync-santhej-web-watchdog",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/santhej~web-watchdog/run-sync": {
      "post": {
        "operationId": "run-sync-santhej-web-watchdog",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "properties": {
          "targets": {
            "title": "Targets",
            "maxItems": 5000,
            "type": "array",
            "description": "URLs to watch — one per line. For per-URL control (a CSS selector, a JSON path, your own knownHash) use the Advanced targets field instead; entries there are added to these. A bare URL becomes {url, watchId, mode: \"auto\"}, where watchId is the first 12 hex characters of sha256 over the CANONICAL url — lower-cased scheme and host, default port and any user:password credentials removed, fragment removed, query parameters sorted by name — so two spellings of the same URL join the same watch. Pass your own `watchId` if you would rather not reproduce that. Max 5,000 per run. Supplying knownHash on EVERY target switches the run to stateless mode: no key-value store is touched, it works under any token scope, and the caller holds the state — this is the shape an MCP tool call wants.",
            "default": [
              "https://registry.npmjs.org/apify"
            ],
            "items": {
              "type": "string"
            }
          },
          "mode": {
            "title": "Detection mode",
            "enum": [
              "auto",
              "conditional_get_hash",
              "json_path",
              "css_selector",
              "rss_guid",
              "sitemap_lastmod",
              "full_body_hash"
            ],
            "type": "string",
            "description": "How to derive the watched value. `auto` picks per response Content-Type: JSON → JSON path / whole-document hash, RSS/Atom → item-set diff, XML sitemap → lastmod, HTML → normalised page hash. Per-target `mode` overrides this.",
            "default": "auto"
          },
          "sensitivity": {
            "title": "Sensitivity",
            "enum": [
              "low",
              "normal",
              "high"
            ],
            "type": "string",
            "description": "Drives the semantic trigger set, not a magnitude float. A change is emitted, delivered and billed if ANY trigger fires: the change was inside your selector/jsonPath (always significant), a number moved by at least the percentage threshold, at least N lines changed, an RSS/sitemap item was added or removed, or availability flipped. `triggerReasons` on every row tells you which fired. A line-count-only change on an UNSCOPED watch must also clear a magnitude floor — the fraction of the document that moved — so an A/B copy swap on a long page is not billed; every other trigger ignores the floor. low = 1.0% / 10 lines / 2% of the document, normal = 0.5% / 2 lines / 0.3% of the document, high = any changed line, no floor.",
            "default": "normal"
          },
          "webhookUrl": {
            "title": "Webhook URL",
            "type": "string",
            "description": "The actor POSTs one flat JSON body per emitted change event and stays completely silent when nothing changed — so firing IS the signal. Point it at an n8n Webhook node or an agent callback. Validated by the egress guard: public hosts only, https strongly recommended.",
            "default": ""
          },
          "webhookSecret": {
            "title": "Webhook secret",
            "type": "string",
            "description": "When set, each POST carries `X-Watchdog-Signature: sha256=<HMAC-SHA256 of the exact raw body>` so the receiver can verify the request came from this actor."
          },
          "maxChangeEvents": {
            "title": "Max billable change events per run",
            "minimum": 0,
            "maximum": 5000,
            "type": "integer",
            "description": "Runaway protection for the one uncapped pricing term. Once hit, the run stops charging, keeps checking, and sets `budgetCapped: true` in the run summary. Default 500 → a worst-case $4.00 per run.",
            "default": 500
          },
          "maxBudgetUsd": {
            "title": "Max budget per run (USD)",
            "minimum": 0,
            "type": "number",
            "description": "Pre-flight abort. Before any fetching the actor computes actor-start + targets × target-checked + min(maxChangeEvents, targets) × change-detected; if that worst case exceeds this number the run aborts immediately with a clear message rather than part-way through a sweep. The min() is deliberate: each target is checked once per run, so a run can never emit more change events than it has targets. Strongly recommended whenever `autoSchedule` is on."
          },
          "emitUnchanged": {
            "title": "Also emit unchanged targets",
            "type": "boolean",
            "description": "Push a `no_change` row per unchanged target too. Off by default so a quiet run produces an empty dataset — cheap to poll, unambiguous to branch on. Never affects billing.",
            "default": false
          },
          "stateKey": {
            "title": "State key",
            "type": "string",
            "description": "Name suffix for the durable named key-value store `wd-state-{stateKey}`. Leave empty and it is derived from the sha256 of the sorted target URL set, so re-running the same list resumes the same watch with zero bookkeeping. Ignored in stateless mode.",
            "default": ""
          },
          "statelessMode": {
            "title": "Stateless mode",
            "type": "boolean",
            "description": "Never open a named store: each target's `knownHash` is the sole prior state and `newHash` is returned on every row for you to persist. Auto-enabled when every target carries `knownHash`, and used automatically as a fallback when named-store creation fails under a restricted scoped token. Note: quarantine, changeStreak and revalidation-miss tracking do not exist in this mode — `maxChangeEvents` and `maxBudgetUsd` are your only spend limits.",
            "default": false
          },
          "intervalMinutes": {
            "title": "Check interval (minutes)",
            "minimum": 5,
            "maximum": 10080,
            "type": "integer",
            "description": "Cadence used to build the cron expression for `autoSchedule` and for the ready-to-POST schedule request body in the run summary. Also used to compute the expected gap between checks for staleness warnings.",
            "default": 60
          },
          "advancedTargets": {
            "title": "Advanced targets (per-URL control)",
            "maxItems": 5000,
            "type": "array",
            "description": "Targets that need more than a URL, as JSON objects, ADDED to the plain URLs above. Each accepts {\"url\", \"watchId\", \"selector\", \"jsonPath\", \"mode\", \"knownHash\", \"knownEtag\", \"knownLastModified\", \"headers\"} — for example {\"url\":\"https://registry.npmjs.org/crawlee\",\"jsonPath\":\"dist-tags.latest\"} to watch one field instead of the whole document, or {\"url\":\"https://example.com/pricing\",\"selector\":\"#price-table\"} to watch one element. Supplying knownHash on EVERY target across both fields switches the run to stateless mode: no key-value store is touched, it works under any token scope, and the caller holds the state — this is the shape an MCP tool call wants.",
            "default": []
          },
          "noiseQuarantineRuns": {
            "title": "Auto-quarantine after N changed runs",
            "minimum": 0,
            "maximum": 1000,
            "type": "integer",
            "description": "A target that changes on this many consecutive runs is auto-quarantined: still checked and still reported as `changeType: \"quarantined\"`, but NOT billed, until it goes one full run without changing. Set 0 to disable. Stateful mode only. Anything above 1000 is clamped to 1000.",
            "default": 5
          },
          "concurrency": {
            "title": "Global concurrency",
            "minimum": 1,
            "maximum": 100,
            "type": "integer",
            "description": "Parallel request cap across all hosts. 50 was measured at 109 MB peak RSS. Per-host concurrency is separately clamped to 2 with jitter regardless of this value, so 1,000 URLs on one host is slow by design.",
            "default": 50
          },
          "maxBodyBytes": {
            "title": "Max response body bytes",
            "minimum": 1024,
            "maximum": 52428800,
            "type": "integer",
            "description": "Abort a download past this and emit `body_too_large`, suggesting a `jsonPath` or `selector`. Caps the transfer line, which is the largest cost component. The 5 MB default accommodates large package-registry documents.",
            "default": 5242880
          },
          "githubToken": {
            "title": "GitHub token",
            "type": "string",
            "description": "Sent as `Authorization: Bearer` on api.github.com targets ONLY. Unauthenticated GitHub is 60 requests/hour per IP and every run may present a different egress IP, so watching more than a handful of repos without this produces intermittent `rate_limited` rows."
          },
          "proxyConfiguration": {
            "title": "Proxy configuration",
            "type": "object",
            "description": "Optional IP rotation if repeat-polling trips a host rate limit. Every recommended target class was verified reachable from a bare datacenter IP, so this is off by default. RESIDENTIAL groups are downgraded to DATACENTER with a warning — residential bandwidth would cost more than the entire rest of the run.",
            "default": {
              "useApifyProxy": false
            }
          },
          "autoSchedule": {
            "title": "Create or update an Apify Schedule",
            "type": "boolean",
            "description": "Create or update exactly one idempotent Apify Schedule named `watch-{stateKey}` at `intervalMinutes`, and return its id. Off by default because schedules are your quota (100 on Starter) and silently consuming it would break your other automations. When off, the run summary still contains a ready-to-POST schedule request body so an agent can create it in one call.",
            "default": false
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}