{"openapi":"3.0.0","info":{"title":"DeltaWatch API","version":"1.0.0","description":"Web change detection API. Authenticate via the session cookie and companion CSRF cookie set by /api/auth/verify, then send x-csrf-token on mutating session-authenticated requests. API key auth via x-api-key is available when enabled in workspace settings and does not require CSRF."},"servers":[{"url":"/api","description":"API base"}],"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"session","description":"Session token from magic link authentication; used together with the csrf cookie for mutating browser requests"},"csrfHeader":{"type":"apiKey","in":"header","name":"x-csrf-token","description":"CSRF token that must match the csrf cookie issued alongside the session cookie"},"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key (must be enabled in settings)"}},"schemas":{"Watch":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"paused":{"type":"boolean"},"checkInterval":{"type":"number","description":"Check interval in minutes (minimum 5)"},"captureMode":{"type":"string","enum":["text","html","json"],"description":"Default capture mode when no selector/JSONPath override is used"},"method":{"type":"string","enum":["GET","POST","PUT"]},"headers":{"type":"object","additionalProperties":{"type":"string"}},"body":{"type":"string"},"cssSelector":{"type":"string"},"xpathSelector":{"type":"string"},"jsonPath":{"type":"string","description":"JSONPath expression for JSON/API monitoring"},"textExtractRegex":{"type":"string"},"ignoreTextRegex":{"type":"string"},"trimWhitespace":{"type":"boolean"},"sortLines":{"type":"boolean"},"removeDuplicateLines":{"type":"boolean"},"ignoreWhitespaceChanges":{"type":"boolean"},"triggerText":{"type":"string"},"triggerTextAbsent":{"type":"string"},"filterAdditions":{"type":"boolean"},"filterRemovals":{"type":"boolean"},"filterReplacements":{"type":"boolean"},"conditions":{"type":"array","items":{"$ref":"#/components/schemas/Condition"}},"conditionsMatchLogic":{"type":"string","enum":["all","any"]},"minChangePercent":{"type":"number","description":"0-100, ignore changes below this threshold"},"notificationUrls":{"type":"array","items":{"type":"string"},"description":"Secret-bearing values are redacted in GET responses"},"notifyByEmail":{"type":"boolean"},"notificationEmails":{"type":"array","items":{"type":"string"}},"notificationMuted":{"type":"boolean"},"sharingEnabled":{"type":"boolean"},"shareToken":{"type":"string","description":"Owner-visible invitation token for shared-watch subscriptions"},"lastChecked":{"type":"string","format":"date-time"},"lastChanged":{"type":"string","format":"date-time"},"lastError":{"type":"string"},"errorCount":{"type":"number"},"previousHash":{"type":"string"},"snapshotCount":{"type":"number"},"hasUnviewed":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","format":"email"},"updatedAt":{"type":"string","format":"date-time"}}},"Condition":{"type":"object","properties":{"field":{"type":"string","enum":["text","added_lines","removed_lines","change_percent"]},"operator":{"type":"string","enum":[">","<",">=","<=","==","!=","contains","not_contains"]},"value":{"type":"string"}},"required":["field","operator","value"]},"Tag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"color":{"type":"string"},"notificationUrls":{"type":"array","items":{"type":"string"}},"overridesWatch":{"type":"boolean"}}},"Workspace":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","format":"email"},"updatedAt":{"type":"string","format":"date-time"}}},"User":{"type":"object","properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string","enum":["admin","user"]},"isPlatformOwner":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"},"notificationEmail":{"type":"string","format":"email"}}},"GlobalSettings":{"type":"object","properties":{"defaultCheckInterval":{"type":"number"},"defaultNotificationUrls":{"type":"array","items":{"type":"string"},"description":"Secret-bearing values are redacted in GET responses"},"emailNotificationsEnabled":{"type":"boolean","description":"Workspace-wide master switch for email alerts"},"defaultNotifyByEmail":{"type":"boolean","description":"Default email-notification state for new watches in this workspace"},"preferredLanguage":{"type":"string","description":"Optional Accept-Language header to send for watches in this workspace unless the watch already sets its own Accept-Language header"},"userAgent":{"type":"string","description":"Optional User-Agent header to send for watches in this workspace unless the watch already sets its own User-Agent header. Leave blank to avoid sending an explicit workspace-level User-Agent."},"stripPageChrome":{"type":"boolean","description":"When enabled, remove common headers, footers, navigation, and skip links before diffing text-mode watches in this workspace"},"stripHiddenItems":{"type":"boolean","description":"When enabled, remove hidden HTML content such as display:none, hidden, and aria-hidden elements before extracting text for watches in this workspace"},"globalIgnoreTextRegex":{"type":"string","description":"Optional regex used to drop matching text lines across all watches in the workspace before comparison"},"globalSubtractiveSelectors":{"type":"string","description":"Optional CSS selectors removed from HTML before extraction across all watches in the workspace, useful for stripping recurring footers or navigation chrome"},"maxSnapshotRetention":{"type":"number"},"minChangePercent":{"type":"number"},"apiKeyEnabled":{"type":"boolean"},"apiKey":{"type":"string","description":"Redacted in GET responses and preserved when the redacted value is sent back unchanged"}}},"DiffResult":{"type":"object","properties":{"added":{"type":"number"},"removed":{"type":"number"},"changed":{"type":"boolean"},"changedLines":{"type":"number"},"contextLines":{"type":"number"},"mode":{"type":"string","enum":["line","word","structured"]},"hunks":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["added","removed","unchanged","modified"]},"value":{"type":"string"},"tokens":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["added","removed","unchanged"]},"value":{"type":"string"}}}}}}}}},"SnapshotArtifact":{"type":"object","properties":{"kind":{"type":"string","enum":["raw_text","html","json","pdf","image","screenshot","binary"]},"key":{"type":"string"},"contentType":{"type":"string"},"byteLength":{"type":"number"}}},"SnapshotRecord":{"type":"object","properties":{"id":{"type":"string"},"watchId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"timestamp":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"source":{"type":"string","enum":["live_check","import","calibration"]},"processorId":{"type":"string"},"kind":{"type":"string","enum":["normalized_text","raw_text","json","html","pdf_text","binary","screenshot"]},"contentType":{"type":"string"},"encoding":{"type":"string"},"byteLength":{"type":"number"},"normalizedHash":{"type":"string"},"rawHash":{"type":"string"},"baseline":{"type":"boolean"},"comparable":{"type":"boolean"},"payloadKey":{"type":"string"},"rawPayloadKey":{"type":"string"},"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotArtifact"}},"summary":{"type":"object","properties":{"changed":{"type":"boolean"},"additions":{"type":"number","nullable":true},"removals":{"type":"number","nullable":true},"changedLines":{"type":"number","nullable":true},"changePercent":{"type":"number"},"comparedToTimestamp":{"type":"string"}}}}},"AssociatedDomain":{"type":"object","properties":{"domain":{"type":"string"},"dismissed":{"type":"boolean"},"viewedAt":{"type":"string","format":"date-time"},"linkedSiteId":{"type":"string","format":"uuid"},"showCrossSiteEdges":{"type":"boolean"}}},"Site":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"},"rootUrl":{"type":"string","format":"uri"},"maxPages":{"type":"number"},"maxDepth":{"type":"number"},"maxEdges":{"type":"number"},"autoWatch":{"type":"boolean"},"respectRobotsTxt":{"type":"boolean"},"crawlRateMs":{"type":"number"},"crawlConcurrency":{"type":"number"},"stripQueryStrings":{"type":"boolean"},"defaultCheckInterval":{"type":"number"},"defaultCaptureMode":{"type":"string","enum":["text","html","json"]},"defaultNotificationUrls":{"type":"array","items":{"type":"string"}},"defaultStripPageChrome":{"type":"boolean"},"defaultStripHiddenItems":{"type":"boolean"},"defaultTrimWhitespace":{"type":"boolean"},"notificationUrls":{"type":"array","items":{"type":"string"}},"notifyByEmail":{"type":"boolean"},"notificationMuted":{"type":"boolean"},"notifyOnCrawlReport":{"type":"boolean"},"notifyOnNewPages":{"type":"boolean"},"showAssociatedDomains":{"type":"boolean"},"associatedDomains":{"type":"array","items":{"$ref":"#/components/schemas/AssociatedDomain"}},"associatedSiteIds":{"type":"array","items":{"type":"string","format":"uuid"}},"lastCrawledAt":{"type":"string","format":"date-time"},"nextScheduledRecrawlAt":{"type":"string","format":"date-time"},"layoutComputedAt":{"type":"string","format":"date-time"},"layoutVersion":{"type":"number"},"layoutNodeCount":{"type":"number"},"layoutEdgeCount":{"type":"number"},"layoutDirty":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","format":"email"},"updatedAt":{"type":"string","format":"date-time"}}},"SiteNode":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"siteId":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"path":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["discovered","reviewed","watched","ignored","blocked","error"]},"watchId":{"type":"string","format":"uuid"},"discoveredAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"crawlDepth":{"type":"number"},"inboundCount":{"type":"number"},"outboundCount":{"type":"number"},"importance":{"type":"number"},"layoutX":{"type":"number"},"layoutY":{"type":"number"},"errorMessage":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SiteEdge":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"siteId":{"type":"string","format":"uuid"},"sourceNodeId":{"type":"string","format":"uuid"},"targetNodeId":{"type":"string","format":"uuid"},"targetUrl":{"type":"string","format":"uri"},"anchorText":{"type":"string"},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"isExternal":{"type":"boolean"},"targetSiteId":{"type":"string","format":"uuid"}}},"CrawlJob":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"siteId":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","completed","failed","cancelled"]},"progress":{"type":"object","properties":{"phase":{"type":"string","enum":["queued","fetching_robots","crawling","completed","failed","cancelled"]},"pagesDiscovered":{"type":"number"},"pagesCrawled":{"type":"number"},"pagesQueued":{"type":"number"},"edgesCreated":{"type":"number"},"externalDomainsFound":{"type":"number"},"erroredPages":{"type":"number"},"blockedPages":{"type":"number"},"linksMappedThisRun":{"type":"number"},"currentUrl":{"type":"string","format":"uri"},"lastProgressAt":{"type":"string","format":"date-time"}}},"checkpoint":{"type":"object","properties":{"queuedUrls":{"type":"array","items":{"type":"string","format":"uri"}},"visitedUrls":{"type":"array","items":{"type":"string","format":"uri"}},"inFlightUrls":{"type":"array","items":{"type":"string","format":"uri"}},"robotsTxtCache":{"type":"string"}}},"siteDetails":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"error":{"type":"string"}}},"WatchCheckJob":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"requestedBy":{"type":"string","format":"email"},"source":{"type":"string","enum":["scheduler","watch_create","manual_bulk","import"]},"watchIds":{"type":"array","items":{"type":"string","format":"uuid"}},"status":{"type":"string","enum":["queued","running","completed","failed"]},"retryState":{"type":"string","enum":["perm","queued","running"]},"progress":{"type":"object","properties":{"phase":{"type":"string"},"totalWatches":{"type":"number"},"processedWatches":{"type":"number"},"checked":{"type":"number"},"changed":{"type":"number"},"errors":{"type":"number"},"skipped":{"type":"number"}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"error":{"type":"string"}}},"ImportJob":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"requestedBy":{"type":"string","format":"email"},"source":{"type":"string","enum":["changedetection","csv","json","distill","urls"]},"mode":{"type":"string","enum":["skip","replace"]},"fileName":{"type":"string"},"fileSize":{"type":"number"},"status":{"type":"string","enum":["queued","running","completed","failed"]},"progress":{"type":"object"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"result":{"type":"object"},"error":{"type":"string"}}},"WorkspaceExportJob":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"workspaceName":{"type":"string"},"requestedBy":{"type":"string","format":"email"},"status":{"type":"string","enum":["queued","running","completed","failed"]},"progress":{"type":"object"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"fileName":{"type":"string"},"manifestChunkCount":{"type":"number"},"artifactChunkCount":{"type":"number"},"artifactSize":{"type":"number"},"error":{"type":"string"}}},"WorkspaceDeleteJob":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"workspaceName":{"type":"string"},"requestedBy":{"type":"string","format":"email"},"ownerType":{"type":"string","enum":["user","system"]},"origin":{"type":"string","enum":["workspace_delete","account_delete","platform_user_delete","platform_workspace_delete"]},"status":{"type":"string","enum":["queued","running","completed","failed"]},"progress":{"type":"object"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"error":{"type":"string"}}},"OperationsOverview":{"type":"object","properties":{"workspaceDeleteJobs":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceDeleteJob"}},"importJobs":{"type":"array","items":{"$ref":"#/components/schemas/ImportJob"}},"platformDeleteJobs":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceDeleteJob"}},"watchCheckJobs":{"type":"array","items":{"$ref":"#/components/schemas/WatchCheckJob"}},"crawlJobs":{"type":"array","items":{"$ref":"#/components/schemas/CrawlJob"}}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}},"WatchShareSubscriber":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"createdAt":{"type":"string","format":"date-time"},"confirmedAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["pending","confirmed"]}}},"WatchShareOwnerView":{"type":"object","properties":{"sharingEnabled":{"type":"boolean"},"shareUrl":{"type":"string"},"currentSubscribers":{"type":"number"},"maxSubscribers":{"type":"number","description":"0 means unlimited for the current plan"},"subscribers":{"type":"array","items":{"$ref":"#/components/schemas/WatchShareSubscriber"}}}},"PublicWatchSharePreview":{"type":"object","properties":{"token":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"ownerEmail":{"type":"string","format":"email"},"latestTimestamp":{"type":"string"},"previousTimestamp":{"type":"string"},"diff":{"allOf":[{"$ref":"#/components/schemas/DiffResult"}],"nullable":true}}},"PublicShareSubscriptionResponse":{"type":"object","properties":{"status":{"type":"string","enum":["pending","confirmed"]},"resendAfterMs":{"type":"number"}}},"PublicShareConfirmResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"email":{"type":"string","format":"email"},"watchId":{"type":"string","format":"uuid"},"confirmedAt":{"type":"string","format":"date-time"},"shareToken":{"type":"string"},"shareUrl":{"type":"string"}}},"PublicShareUnsubscribePreview":{"type":"object","properties":{"ok":{"type":"boolean"},"email":{"type":"string","format":"email"},"watchTitle":{"type":"string"}}}}},"paths":{"/auth/request":{"post":{"tags":["Auth"],"summary":"Request magic link","description":"Sends a magic link to the provided email address for passwordless authentication. Rate-limited to 3 per email and 10 per IP within 15 minutes. If ALLOWED_EMAIL_DOMAINS is configured, self-serve sign-in is restricted to approved domains unless the user has already been invited.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Request accepted (always returns success to avoid email enumeration)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"description":"Invalid email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Email domain not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/verify":{"post":{"tags":["Auth"],"summary":"Verify magic link token","description":"Verifies a magic link token after an explicit confirmation step and sets session and CSRF cookies.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"Magic link token from email"},"email":{"type":"string","format":"email","description":"Optional extra confirmation of the target email address"}}}}}},"responses":{"200":{"description":"Authentication successful. Sets session and CSRF cookies.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Token missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/logout":{"post":{"tags":["Auth"],"summary":"Log out","description":"Invalidates the current session and clears the session and CSRF cookies.","security":[],"responses":{"200":{"description":"Logged out","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/auth/me":{"get":{"tags":["Auth"],"summary":"Get current user","description":"Returns the currently authenticated user profile.","responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Auth"],"summary":"Update current user profile","description":"Updates the current user name. Requires session auth and a matching CSRF token when using browser cookies.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated user profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Name is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/workspace":{"put":{"tags":["Auth"],"summary":"Rename current workspace","description":"Renames the current workspace. Requires workspace admin rights and a matching CSRF token when using browser cookies.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"400":{"description":"Workspace name is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Workspace not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Auth"],"summary":"Switch active workspace","description":"Switches the active workspace for the current session. The server rotates the session and CSRF cookies and returns the hydrated user for the new workspace.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Workspace switched","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"user":{"$ref":"#/components/schemas/User"}}}}}},"400":{"description":"Workspace ID required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Workspace not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches":{"get":{"tags":["Watches"],"summary":"List watches","description":"Returns a paginated, filterable, sortable list of all watches.","parameters":[{"name":"search","in":"query","schema":{"type":"string"},"description":"Filter by title or URL (case-insensitive)"},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag name"},{"name":"status","in":"query","schema":{"type":"string","enum":["changed","errors","paused"]},"description":"Filter by status"},{"name":"sort","in":"query","schema":{"type":"string","enum":["title","lastChecked"]},"description":"Sort field (default: lastChanged)"},{"name":"page","in":"query","schema":{"type":"integer","default":1},"description":"Page number"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100},"description":"Items per page"}],"responses":{"200":{"description":"Paginated list of watches","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Watch"}},"watches":{"type":"array","items":{"$ref":"#/components/schemas/Watch"},"description":"Alias for items"},"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"post":{"tags":["Watches"],"summary":"Create a watch","description":"Creates a new URL watch. The URL must start with http:// or https://.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"paused":{"type":"boolean"},"checkInterval":{"type":"number"},"captureMode":{"type":"string","enum":["text","html","json"]},"method":{"type":"string","enum":["GET","POST","PUT"]},"headers":{"type":"object","additionalProperties":{"type":"string"}},"body":{"type":"string"},"cssSelector":{"type":"string"},"xpathSelector":{"type":"string"},"textExtractRegex":{"type":"string"},"ignoreTextRegex":{"type":"string"},"trimWhitespace":{"type":"boolean"},"sortLines":{"type":"boolean"},"removeDuplicateLines":{"type":"boolean"},"ignoreWhitespaceChanges":{"type":"boolean"},"triggerText":{"type":"string"},"triggerTextAbsent":{"type":"string"},"filterAdditions":{"type":"boolean"},"filterRemovals":{"type":"boolean"},"filterReplacements":{"type":"boolean"},"conditions":{"type":"array","items":{"$ref":"#/components/schemas/Condition"}},"conditionsMatchLogic":{"type":"string","enum":["all","any"]},"minChangePercent":{"type":"number"},"notificationUrls":{"type":"array","items":{"type":"string"},"description":"Secret-bearing values are redacted in GET responses"},"notifyByEmail":{"type":"boolean"},"notificationEmails":{"type":"array","items":{"type":"string"}},"notificationMuted":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Watch created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Watch"}}}},"400":{"description":"Invalid URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/search":{"get":{"tags":["Watches"],"summary":"Search watches","description":"Deep search across title, URL, error text, and tags.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"Search query"}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"type":"object","properties":{"watches":{"type":"array","items":{"$ref":"#/components/schemas/Watch"}},"total":{"type":"integer"}}}}}},"400":{"description":"Missing query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/bulk/recheck":{"post":{"tags":["Watches"],"summary":"Bulk recheck","description":"Triggers a recheck for multiple watches. If no IDs are provided, rechecks all non-paused watches.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Watch IDs to recheck (omit for all)"}}}}}},"responses":{"200":{"description":"Recheck results","content":{"application/json":{"schema":{"type":"object","properties":{"checked":{"type":"integer"},"changed":{"type":"integer"}}}}}}}}},"/watches/bulk/pause":{"post":{"tags":["Watches"],"summary":"Bulk pause/unpause","description":"Pauses or unpauses multiple watches. If no IDs are provided, applies to all watches.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["paused"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Watch IDs (omit for all)"},"paused":{"type":"boolean","description":"true to pause, false to unpause"}}}}}},"responses":{"200":{"description":"Update result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}}}}}}}}},"/watches/bulk/mute":{"post":{"tags":["Watches"],"summary":"Bulk mute/unmute notifications","description":"Mutes or unmutes notifications for multiple watches. If no IDs are provided, applies to all watches.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["muted"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Watch IDs (omit for all)"},"muted":{"type":"boolean","description":"true to mute, false to unmute"}}}}}},"responses":{"200":{"description":"Update result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}}}}}}}}},"/watches/bulk/viewed":{"post":{"tags":["Watches"],"summary":"Bulk mark viewed","description":"Clears the unviewed flag for multiple watches. If no IDs are provided, applies to all watches in the workspace.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Watch IDs to mark viewed (omit for all)"}}}}}},"responses":{"200":{"description":"Update result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}}}}}}}}},"/watches/bulk/notifications":{"post":{"tags":["Watches"],"summary":"Bulk overwrite notification settings","description":"Overwrites notification settings for multiple watches. If no IDs are provided, applies to all watches.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["notifyByEmail","notificationMuted","notificationUrls"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Watch IDs (omit for all)"},"notifyByEmail":{"type":"boolean","description":"Whether email alerts should be sent for the selected watches"},"notificationMuted":{"type":"boolean","description":"Whether all notifications should be muted for the selected watches"},"notificationUrls":{"type":"array","items":{"type":"string"},"description":"Replacement notification URL list for the selected watches"}}}}}},"responses":{"200":{"description":"Update result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}}}}}}}}},"/watches/bulk/delete":{"post":{"tags":["Watches"],"summary":"Bulk delete","description":"Deletes multiple watches and their snapshots.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Watch IDs to delete"}}}}}},"responses":{"200":{"description":"Delete result","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"integer"}}}}}},"400":{"description":"Missing IDs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}":{"get":{"tags":["Watches"],"summary":"Get a watch","description":"Returns a single watch by ID.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Watch details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Watch"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Watches"],"summary":"Update a watch","description":"Updates an existing watch. Only provided fields are changed; id, createdAt, and createdBy are immutable.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Any editable watch fields (url, title, tags, paused, checkInterval, captureMode, method, headers, body, cssSelector, xpathSelector, jsonPath, textExtractRegex, ignoreTextRegex, trimWhitespace, sortLines, removeDuplicateLines, ignoreWhitespaceChanges, triggerText, triggerTextAbsent, filterAdditions, filterRemovals, filterReplacements, conditions, conditionsMatchLogic, minChangePercent, notificationUrls, notifyByEmail, notificationEmails, notificationMuted). Secret-bearing notification URLs are redacted in GET responses and can be preserved by sending the redacted value back unchanged."}}}},"responses":{"200":{"description":"Updated watch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Watch"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Watches"],"summary":"Delete a watch","description":"Deletes a watch and all its snapshots.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/recheck":{"post":{"tags":["Watches"],"summary":"Trigger recheck","description":"Immediately rechecks a watch, computes diff if changed, and sends notifications if conditions are met.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Recheck result","content":{"application/json":{"schema":{"type":"object","properties":{"changed":{"type":"boolean"},"diff":{"$ref":"#/components/schemas/DiffResult"},"notified":{"type":"boolean"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/viewed":{"post":{"tags":["Watches"],"summary":"Mark as viewed","description":"Clears the unviewed flag on a watch.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Marked as viewed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/share":{"get":{"tags":["Watches"],"summary":"Get shared-watch owner state","description":"Returns sharing state, invitation link, and subscriber list for the specified watch.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sharing details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchShareOwnerView"}}}},"403":{"description":"Sharing not available on the current plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/share/enable":{"post":{"tags":["Watches"],"summary":"Enable watch sharing","description":"Enables invitation-by-link watch sharing and creates a share token if needed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sharing enabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchShareOwnerView"}}}},"403":{"description":"Sharing not available on the current plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/share/disable":{"post":{"tags":["Watches"],"summary":"Disable watch sharing","description":"Disables invitation-by-link watch sharing for the specified watch.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sharing disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchShareOwnerView"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/share/rotate":{"post":{"tags":["Watches"],"summary":"Rotate watch share link","description":"Creates a new invitation token for the watch. Optionally revokes all current subscribers.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"revokeSubscribers":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Share link rotated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchShareOwnerView"}}}},"403":{"description":"Sharing not available on the current plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/share/revoke-all":{"post":{"tags":["Watches"],"summary":"Remove all shared-watch subscribers","description":"Deletes all pending and confirmed subscribers for the watch while leaving sharing enabled.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Subscribers removed","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WatchShareOwnerView"},{"type":"object","properties":{"deleted":{"type":"number"}}}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{id}/share/subscribers/{subscriptionId}":{"delete":{"tags":["Watches"],"summary":"Remove one shared-watch subscriber","description":"Deletes a single subscriber from the watch share list.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Subscriber removed","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WatchShareOwnerView"},{"type":"object","properties":{"ok":{"type":"boolean"}}}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/share/{token}":{"get":{"tags":["Public Share"],"summary":"Get shared-watch preview","description":"Returns public preview data for an invitation-only shared watch, including the watched URL and latest diff preview.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Shared-watch preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWatchSharePreview"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/share/{token}/subscribe":{"post":{"tags":["Public Share"],"summary":"Subscribe to a shared watch by email","description":"Creates or refreshes a pending email subscription and sends a confirmation email.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Already confirmed or still pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicShareSubscriptionResponse"}}}},"202":{"description":"Confirmation email sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicShareSubscriptionResponse"}}}},"400":{"description":"Invalid email address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Sharing unavailable or subscriber cap reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/share/confirm/{confirmToken}":{"get":{"tags":["Public Share"],"summary":"Confirm a shared-watch subscription","description":"Confirms a pending shared-watch email subscription.","security":[],"parameters":[{"name":"confirmToken","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Subscription confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicShareConfirmResponse"}}}},"403":{"description":"Subscriber cap reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Expired or invalid confirmation link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/share/unsubscribe/{unsubscribeToken}":{"get":{"tags":["Public Share"],"summary":"Preview a shared-watch unsubscribe","description":"Returns the email address and watch title for an unsubscribe confirmation page.","security":[],"parameters":[{"name":"unsubscribeToken","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unsubscribe preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicShareUnsubscribePreview"}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Public Share"],"summary":"Complete a shared-watch unsubscribe","description":"Deletes a shared-watch subscription after the recipient confirms the action.","security":[],"parameters":[{"name":"unsubscribeToken","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Subscription removed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{watchId}/history":{"get":{"tags":["History"],"summary":"List snapshots","description":"Returns a paginated list of snapshots for a watch, sorted newest first.","parameters":[{"name":"watchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"Paginated snapshot list","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"string"},"date":{"type":"string","format":"date-time"},"key":{"type":"string"}}}},"snapshots":{"type":"array","description":"Alias for items"},"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"}}}}}},"404":{"description":"Watch not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{watchId}/history/{timestamp}":{"get":{"tags":["History"],"summary":"Get snapshot","description":"Returns the content of a specific snapshot.","parameters":[{"name":"watchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"timestamp","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot content","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"},"timestamp":{"type":"string"}}}}}},"404":{"description":"Snapshot not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{watchId}/history/{timestamp}/meta":{"get":{"tags":["History"],"summary":"Get snapshot metadata","description":"Returns the full metadata record for a specific snapshot.","parameters":[{"name":"watchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"timestamp","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotRecord"}}}},"404":{"description":"Watch or metadata not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{watchId}/history/{timestamp}/artifacts":{"get":{"tags":["History"],"summary":"List snapshot artifacts","description":"Returns the artifact references associated with a specific snapshot.","parameters":[{"name":"watchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"timestamp","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot artifacts","content":{"application/json":{"schema":{"type":"object","properties":{"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotArtifact"}}}}}}},"404":{"description":"Watch or metadata not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/watches/{watchId}/diff/{from}/{to}":{"get":{"tags":["History"],"summary":"Get diff between snapshots","description":"Computes a diff between two snapshots. Use \"previous\" for {from} and \"latest\" for {to} as shortcuts.","parameters":[{"name":"watchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"from","in":"path","required":true,"schema":{"type":"string"},"description":"Snapshot timestamp or \"previous\""},{"name":"to","in":"path","required":true,"schema":{"type":"string"},"description":"Snapshot timestamp or \"latest\""},{"name":"wordLevel","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"Enable word-level diffing"}],"responses":{"200":{"description":"Diff result with both snapshot contents","content":{"application/json":{"schema":{"type":"object","properties":{"diff":{"$ref":"#/components/schemas/DiffResult"},"from":{"type":"object","properties":{"timestamp":{"type":"string"},"content":{"type":"string"}}},"to":{"type":"object","properties":{"timestamp":{"type":"string"},"content":{"type":"string"}}}}}}}},"404":{"description":"Snapshot(s) not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tags":{"get":{"tags":["Tags"],"summary":"List tags","description":"Returns all tags.","responses":{"200":{"description":"Tag list","content":{"application/json":{"schema":{"type":"object","properties":{"tags":{"type":"array","items":{"$ref":"#/components/schemas/Tag"}}}}}}}}},"post":{"tags":["Tags"],"summary":"Create a tag","description":"Creates a new tag.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","description":"Tag name (defaults to \"New Tag\")"},"color":{"type":"string"},"overridesWatch":{"type":"boolean","description":"Whether tag notification URLs override per-watch URLs"},"notificationUrls":{"type":"array","items":{"type":"string"},"description":"Secret-bearing values are redacted in GET responses"}}}}}},"responses":{"201":{"description":"Tag created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tag"}}}}}}},"/tags/{id}":{"put":{"tags":["Tags"],"summary":"Update a tag","description":"Updates an existing tag. The tag ID cannot be changed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"color":{"type":"string"},"overridesWatch":{"type":"boolean"},"notificationUrls":{"type":"array","items":{"type":"string"},"description":"Secret-bearing values are redacted in GET responses"}}}}}},"responses":{"200":{"description":"Updated tag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tag"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tags"],"summary":"Delete a tag","description":"Deletes a tag.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/settings":{"get":{"tags":["Settings"],"summary":"Get settings","description":"Returns global settings. Secret-bearing values such as notification URLs and the API key are redacted in the response.","responses":{"200":{"description":"Global settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalSettings"}}}}}},"put":{"tags":["Settings"],"summary":"Update settings","description":"Updates global settings. Requires admin role. Secret-bearing notification URLs and the API key are redacted in GET responses and preserved when submitted unchanged.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalSettings"}}}},"responses":{"200":{"description":"Updated settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalSettings"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/users":{"get":{"tags":["Users"],"summary":"List users","description":"Returns users in the current workspace. Requires admin role in that workspace.","responses":{"200":{"description":"User list","content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/users/invite":{"post":{"tags":["Users"],"summary":"Invite user","description":"Creates a new user if needed, adds them to the current workspace, and sends them a magic link. Requires admin role in that workspace.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string","enum":["admin","user"],"default":"user"}}}}}},"responses":{"201":{"description":"User invited","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"description":"Missing email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"User already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/users/{email}":{"put":{"tags":["Users"],"summary":"Update user","description":"Updates a user within the current workspace. Email cannot be changed. Requires admin role in that workspace.","parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string"},"description":"URL-encoded email"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["admin","user"]}}}}}},"responses":{"200":{"description":"Updated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Users"],"summary":"Delete user","description":"Removes a user from the current workspace. Cannot delete your own account. Requires admin role in that workspace.","parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string"},"description":"URL-encoded email"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Cannot delete self","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/billing":{"get":{"tags":["Billing"],"summary":"Get billing state","description":"Returns the current workspace entitlements, billing state, and tier configuration. Available to all authenticated users.","responses":{"200":{"description":"Billing state","content":{"application/json":{"schema":{"type":"object","properties":{"entitlements":{"type":"object","properties":{"plan":{"type":"string","enum":["ping","pulse","sentinel"]},"planStatus":{"type":"string","enum":["active","cancelled","past_due"]},"maxWatches":{"type":"number","description":"0 means unlimited"},"minCheckInterval":{"type":"number"},"maxSnapshotRetention":{"type":"number","description":"0 means unlimited"},"maxSharedWatchSubscribers":{"type":"number","description":"0 means unlimited"},"currentWatchCount":{"type":"number"},"currentPeriodEnd":{"type":"string","format":"date-time"},"cancelAtPeriodEnd":{"type":"boolean"},"billingEnabled":{"type":"boolean"},"checkoutAvailable":{"type":"boolean"},"watchSharingEnabled":{"type":"boolean"}}},"billing":{"type":"object","properties":{"workspaceId":{"type":"string"},"plan":{"type":"string","enum":["ping","pulse","sentinel"]},"planStatus":{"type":"string"},"currentPeriodEnd":{"type":"string","format":"date-time"},"cancelAtPeriodEnd":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"description":"Billing record with Stripe IDs stripped for security"},"tiers":{"type":"object","description":"Tier limit configuration (ping, pulse, sentinel)"}}}}}}}}},"/billing/checkout":{"post":{"tags":["Billing"],"summary":"Create Stripe checkout session","description":"Creates a Stripe Checkout session for upgrading to a paid plan. Requires workspace admin role. Returns a URL to redirect the user to Stripe.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["pulse","sentinel"],"description":"The plan tier to subscribe to"}}}}}},"responses":{"200":{"description":"Checkout session URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Stripe Checkout URL to redirect the user to"}}}}}},"400":{"description":"Invalid plan or active subscription exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Billing not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/billing/portal":{"post":{"tags":["Billing"],"summary":"Create Stripe customer portal session","description":"Creates a Stripe customer portal session for managing an existing subscription. Requires workspace admin role and an existing Stripe customer. Returns a URL to redirect the user to the portal.","responses":{"200":{"description":"Portal session URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Stripe customer portal URL"}}}}}},"400":{"description":"No billing account found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Billing not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/billing/webhook":{"post":{"tags":["Billing"],"summary":"Stripe webhook","description":"Receives Stripe webhook events for subscription lifecycle management. No authentication required; the request is verified using the Stripe webhook signature. Must be mounted before global JSON body parsing.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","format":"binary","description":"Raw Stripe event payload"}}}},"responses":{"200":{"description":"Event received","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"boolean"}}}}}},"400":{"description":"Missing or invalid signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Webhook not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import/preview":{"post":{"tags":["Import"],"summary":"Preview file import","description":"Parses an upload and returns a no-write preview of what would be created, skipped, replaced, or clamped.","responses":{"200":{"description":"Import preview","content":{"application/json":{"schema":{"type":"object","properties":{"preview":{"type":"object"}}}}}},"400":{"description":"Invalid file or format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import/preview/urls":{"post":{"tags":["Import"],"summary":"Preview URL list import","description":"Returns a no-write preview for a pasted URL list.","responses":{"200":{"description":"Import preview"}}}},"/import/changedetection":{"post":{"tags":["Import"],"summary":"Import changedetection.io backup","description":"Imports watches from a changedetection.io backup ZIP file. Supports skip or replace mode for duplicates.","parameters":[{"name":"mode","in":"query","schema":{"type":"string","enum":["skip","replace"],"default":"skip"},"description":"Duplicate handling mode. Invalid values return 400."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["backup"],"properties":{"backup":{"type":"string","format":"binary","description":"ZIP file (max 256MB)"}}}}}},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"type":"object","properties":{"imported":{"type":"integer"},"skipped":{"type":"integer"},"errors":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Missing file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import/uploads/prepare":{"post":{"tags":["Import"],"summary":"Prepare chunked upload session","description":"Creates an upload session for large background imports.","responses":{"200":{"description":"Upload session prepared"}}}},"/import/uploads/{id}/chunks/{index}":{"put":{"tags":["Import"],"summary":"Upload chunk","description":"Uploads one chunk into a prepared import upload session.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"index","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Chunk stored"}}}},"/import/uploads/{id}/complete":{"post":{"tags":["Import"],"summary":"Complete chunked upload","description":"Finalises a chunked upload and queues the background import job.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Import job queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}}}}},"/import/uploads/{id}":{"delete":{"tags":["Import"],"summary":"Cancel upload session","description":"Deletes an in-progress chunked upload session.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Upload session deleted"}}}},"/import/jobs":{"get":{"tags":["Import"],"summary":"List import jobs","description":"Returns active and recent import jobs for the current workspace.","responses":{"200":{"description":"Import jobs","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/ImportJob"}}}}}}}}}},"/import/jobs/{id}":{"get":{"tags":["Import"],"summary":"Get import job","description":"Returns a single import job for the current workspace.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Import job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import/jobs/{id}/cancel":{"post":{"tags":["Import"],"summary":"Cancel import job","description":"Cancels a queued or running import job.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Import job cancelled"}}}},"/import/urls":{"post":{"tags":["Import"],"summary":"Import URL list","description":"Creates watches from a newline-separated list of URLs.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["urls"],"properties":{"urls":{"type":"string","description":"Newline-separated list of URLs"}}}}}},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"type":"object","properties":{"imported":{"type":"integer"},"skipped":{"type":"integer"},"errors":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Missing URL list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import/csv":{"post":{"tags":["Import"],"summary":"Import CSV","description":"Queues or runs a CSV watch import.","responses":{"202":{"description":"Import job queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}}}}},"/import/json":{"post":{"tags":["Import"],"summary":"Import JSON","description":"Queues or runs a JSON watch import.","responses":{"202":{"description":"Import job queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}}}}},"/import/distill":{"post":{"tags":["Import"],"summary":"Import Distill export","description":"Queues or runs a Distill watch import.","responses":{"202":{"description":"Import job queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}}}}},"/export":{"post":{"tags":["Export"],"summary":"Create export job","description":"Queues a workspace backup export job.","responses":{"202":{"description":"Export job queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceExportJob"}}}}}}},"/export/jobs/latest":{"get":{"tags":["Export"],"summary":"Get latest export job","description":"Returns the most recent export job for the current workspace.","responses":{"200":{"description":"Latest export job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceExportJob"}}}}}}},"/export/jobs/{id}":{"get":{"tags":["Export"],"summary":"Get export job","description":"Returns a specific export job for the current workspace.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Export job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceExportJob"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/export/jobs/{id}/chunks/{index}":{"get":{"tags":["Export"],"summary":"Download export chunk","description":"Downloads a binary artifact chunk from a completed export job.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"index","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Export chunk","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Chunk or job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Export not ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Export expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites":{"get":{"tags":["Sites"],"summary":"List sites","description":"Returns all sites in the current workspace.","responses":{"200":{"description":"Site list","content":{"application/json":{"schema":{"type":"object","properties":{"sites":{"type":"array","items":{"$ref":"#/components/schemas/Site"}}}}}}}}},"post":{"tags":["Sites"],"summary":"Create site","description":"Creates a site and may queue an initial crawl job.","responses":{"201":{"description":"Site created"}}}},"/sites/{id}":{"get":{"tags":["Sites"],"summary":"Get site","description":"Returns a site plus computed counts and current state.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"404":{"description":"Site not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Sites"],"summary":"Update site","description":"Updates site settings and defaults.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated site"},"404":{"description":"Site not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Sites"],"summary":"Delete site","description":"Deletes a site, its graph, and crawl jobs.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Site deleted"}}}},"/sites/{id}/crawl":{"post":{"tags":["Sites"],"summary":"Trigger site crawl","description":"Queues a re-crawl for the site.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Crawl queued","content":{"application/json":{"schema":{"type":"object","properties":{"crawlJobId":{"type":"string","format":"uuid"}}}}}},"409":{"description":"Crawl already active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{id}/graph":{"get":{"tags":["Sites"],"summary":"Get site graph","description":"Returns a paginated graph slice for the site.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Graph slice","content":{"application/json":{"schema":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/SiteNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/SiteEdge"}},"total":{"type":"number"}}}}}}}}},"/sites/{id}/nodes":{"get":{"tags":["Sites"],"summary":"List site nodes","description":"Returns paginated or filtered site nodes for the table and related-pages views.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Site nodes","content":{"application/json":{"schema":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/SiteNode"}},"total":{"type":"number"},"rawTotal":{"type":"number"}}}}}}}}},"/sites/{id}/nodes/{nodeId}":{"put":{"tags":["Sites"],"summary":"Update site node status","description":"Updates a single site node status such as watched, ignored, or reviewed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"nodeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated site node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteNode"}}}}}}},"/sites/{id}/nodes/bulk":{"post":{"tags":["Sites"],"summary":"Bulk update site nodes","description":"Bulk updates site node status for the current filter or selection.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Bulk update result"}}}},"/sites/{id}/nodes/delete":{"post":{"tags":["Sites"],"summary":"Bulk delete site nodes","description":"Deletes selected or filtered site nodes and connected graph edges.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Bulk delete result"}}}},"/sites/{id}/edges":{"get":{"tags":["Sites"],"summary":"List site edges","description":"Returns site graph edges.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Site edges","content":{"application/json":{"schema":{"type":"object","properties":{"edges":{"type":"array","items":{"$ref":"#/components/schemas/SiteEdge"}}}}}}}}}},"/sites/{id}/associated-domains":{"get":{"tags":["Sites"],"summary":"List associated domains","description":"Returns domains linked from the site strongly enough to be follow-up candidates.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Associated domains"}}}},"/sites/{id}/associated-domains/{domain}":{"put":{"tags":["Sites"],"summary":"Update associated domain","description":"Dismisses or re-enables an associated domain candidate.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"domain","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Associated domain updated"}}}},"/sites/{id}/associated-domains/{domain}/create-site":{"post":{"tags":["Sites"],"summary":"Create site from associated domain","description":"Creates a new site from an associated-domain candidate.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"domain","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Related site created"}}}},"/sites/{id}/crawl-jobs":{"get":{"tags":["Sites"],"summary":"List site crawl jobs","description":"Returns recent crawl jobs for a site.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Crawl jobs","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/CrawlJob"}}}}}}}}}},"/operations":{"get":{"tags":["Operations"],"summary":"Operations overview","description":"Returns recent workspace operations including delete jobs, import jobs, watch-check jobs, and crawl jobs.","responses":{"200":{"description":"Operations overview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationsOverview"}}}}}}},"/operations/workspace-delete-jobs/{id}/retry":{"post":{"tags":["Operations"],"summary":"Retry workspace delete job","description":"Retries a failed workspace delete job.","responses":{"200":{"description":"Retry accepted"}}}},"/operations/watch-check-jobs/{id}/retry":{"post":{"tags":["Operations"],"summary":"Retry watch-check job","description":"Queues a fresh watch-check retry for a failed job.","responses":{"200":{"description":"Retry accepted"}}}},"/operations/import-jobs/{id}/dismiss":{"post":{"tags":["Operations"],"summary":"Dismiss import job","description":"Dismisses a completed or failed import job from the operations view.","responses":{"200":{"description":"Job dismissed"}}}},"/operations/watch-check-jobs/{id}/dismiss":{"post":{"tags":["Operations"],"summary":"Dismiss watch-check job","description":"Dismisses a completed or failed watch-check job from the operations view.","responses":{"200":{"description":"Job dismissed"}}}},"/health":{"get":{"tags":["Health"],"summary":"Health check","description":"Returns server health status and consistency tier.","security":[],"responses":{"200":{"description":"Server is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"timestamp":{"type":"string","format":"date-time"},"ct":{"type":"integer","description":"Consistency tier: 1 = eventual (edge-cached, up to 60s propagation), 2 = strong (reads always reflect latest write)","enum":[1,2]}}}}}}}}}},"security":[{"sessionCookie":[]},{"apiKey":[]}]}