{"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"},"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"}}},"PlatformWorkspaceSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","format":"email"},"memberCount":{"type":"number"},"adminCount":{"type":"number"},"watchCount":{"type":"number"},"tagCount":{"type":"number"}}},"PlatformWorkspaceMemberSummary":{"type":"object","properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string","enum":["admin","user"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"},"invitedBy":{"type":"string","format":"email"},"isPlatformOwner":{"type":"boolean"}}},"PlatformWorkspaceWatchSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"paused":{"type":"boolean"},"checkInterval":{"type":"number"},"captureMode":{"type":"string","enum":["text","html","json"]},"lastChecked":{"type":"string","format":"date-time"},"lastChanged":{"type":"string","format":"date-time"},"lastError":{"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"},"tags":{"type":"array","items":{"type":"string"}}}},"PlatformWorkspaceTagSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"color":{"type":"string"},"overridesWatch":{"type":"boolean"},"notificationUrlCount":{"type":"number"}}},"PlatformWorkspaceDetail":{"type":"object","properties":{"workspace":{"$ref":"#/components/schemas/Workspace"},"memberCount":{"type":"number"},"adminCount":{"type":"number"},"watchCount":{"type":"number"},"tagCount":{"type":"number"},"members":{"type":"array","items":{"$ref":"#/components/schemas/PlatformWorkspaceMemberSummary"}},"watches":{"type":"array","items":{"$ref":"#/components/schemas/PlatformWorkspaceWatchSummary"}},"tags":{"type":"array","items":{"$ref":"#/components/schemas/PlatformWorkspaceTagSummary"}}}},"PlatformMembershipSummary":{"type":"object","properties":{"workspaceId":{"type":"string","format":"uuid"},"workspaceName":{"type":"string"},"role":{"type":"string","enum":["admin","user"]}}},"PlatformUserSummary":{"type":"object","properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"},"ownedWorkspaceId":{"type":"string","format":"uuid"},"isPlatformOwner":{"type":"boolean"},"memberships":{"type":"array","items":{"$ref":"#/components/schemas/PlatformMembershipSummary"}}}},"PlatformOverview":{"type":"object","properties":{"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/PlatformWorkspaceSummary"}},"users":{"type":"array","items":{"$ref":"#/components/schemas/PlatformUserSummary"}}}},"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"]},"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"}}}}},"Error":{"type":"object","properties":{"error":{"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/{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"}}}}}}},"/platform":{"get":{"tags":["Platform"],"summary":"Platform overview","description":"Returns all workspaces and all users across the platform. Requires a platform owner email configured in PLATFORM_OWNER_EMAILS.","responses":{"200":{"description":"Platform overview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformOverview"}}}},"403":{"description":"Platform owner required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/platform/workspaces/{id}":{"get":{"tags":["Platform"],"summary":"Workspace detail","description":"Returns a read-only workspace detail view including members, watches, and tags. Requires platform owner access.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Workspace ID"}],"responses":{"200":{"description":"Workspace detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformWorkspaceDetail"}}}},"403":{"description":"Platform owner required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Workspace 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"},"currentWatchCount":{"type":"number"},"currentPeriodEnd":{"type":"string","format":"date-time"},"cancelAtPeriodEnd":{"type":"boolean"},"billingEnabled":{"type":"boolean"},"checkoutAvailable":{"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/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/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"}}}}}}},"/export":{"get":{"tags":["Export"],"summary":"Export backup","description":"Downloads a ZIP backup containing all watches, snapshots, tags, and settings.","responses":{"200":{"description":"ZIP file download","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}}}}},"/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":[]}]}