{
  "openapi": "3.1.0",
  "info": {
    "title": "SpecWatch public API",
    "version": "1.0.0",
    "description": "Machine-readable surface of https://specwatch.me. SpecWatch continuously checks public product pages for broken links, specification changes, and price or availability drift. This document covers the public, unauthenticated endpoints (health check and the one-page scan funnel) plus the site's content routes. The full workspace API behind the web app is session-authenticated; a documented programmatic API for paid plans is on the roadmap — write to contact@specwatch.me if you need it sooner. Human-readable docs: https://specwatch.me/docs.",
    "contact": {
      "name": "SpecWatch",
      "url": "https://specwatch.me/contact",
      "email": "contact@specwatch.me"
    }
  },
  "servers": [
    { "url": "https://specwatch.me", "description": "Production" }
  ],
  "tags": [
    { "name": "public-api", "description": "Callable without an account or API key." },
    { "name": "content", "description": "Human- and agent-readable site pages. Content routes that exist as prose are also served as Markdown via Accept: text/markdown." }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": ["public-api"],
        "operationId": "getHealth",
        "summary": "Liveness check",
        "description": "Returns service health. No authentication, no rate limit. Use this to verify reachability before running scans.",
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "ok": { "type": "boolean", "const": true } },
                  "required": ["ok"],
                  "additionalProperties": false
                },
                "example": { "ok": true }
              }
            }
          }
        }
      }
    },
    "/api/scan": {
      "post": {
        "tags": ["public-api"],
        "operationId": "scanPage",
        "summary": "Check one public product page",
        "description": "Runs the same check the SpecWatch landing page's form runs: outbound links on the page are followed, specification signals are extracted, and the result is returned as JSON with a summary, per-finding detail, and word-level changes against the page's previous scan. No account is needed. Rate-limited to 10 requests per 10 minutes per client IP — read the RateLimit-* response headers and honor Retry-After on 429. Only check pages you have a legitimate interest in monitoring (see the terms of service).",
        "requestBody": {
          "required": true,
          "description": "The public http(s) product page to check.",
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ScanRequest" },
              "example": { "url": "https://example.com/product-page" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The page was checked; the body is the saved scan. A scan takes a few seconds to a minute.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": {
            "description": "The workspace's daily scan budget is spent. Upgrade the plan or retry tomorrow."
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": {
            "description": "The target page could not be fetched (site down, unreachable, or blocked the check)."
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": ["content"],
        "operationId": "getHome",
        "summary": "Home page",
        "description": "What SpecWatch checks (broken links, specification changes, price and availability moves) and how to start. Served as HTML to browsers and as Markdown when the request carries Accept: text/markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/pricing": {
      "get": {
        "tags": ["content"],
        "operationId": "getPricing",
        "summary": "Plans and pricing",
        "description": "Free, Lite, Pro, and Max plans. Every plan runs the same checks; plans differ in how many pages are watched and how often. Also available as Markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/docs": {
      "get": {
        "tags": ["content"],
        "operationId": "getDocs",
        "summary": "API and agent documentation",
        "description": "How to call the public endpoints, rate-limit header conventions, Markdown content negotiation, and error behavior. Also available as Markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/about": {
      "get": {
        "tags": ["content"],
        "operationId": "getAbout",
        "summary": "About SpecWatch",
        "description": "Who builds SpecWatch, the quality mindset behind it, and who it is for. Also available as Markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/contact": {
      "get": {
        "tags": ["content"],
        "operationId": "getContact",
        "summary": "Contact information",
        "description": "Sales and general questions (contact@specwatch.me) and privacy or data requests (ahmedmetered@specwatch.me). Also available as Markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/privacy": {
      "get": {
        "tags": ["content"],
        "operationId": "getPrivacy",
        "summary": "Privacy policy",
        "description": "What SpecWatch collects, what it never collects, service providers, retention, and GDPR rights. Also available as Markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/terms": {
      "get": {
        "tags": ["content"],
        "operationId": "getTerms",
        "summary": "Terms of service",
        "description": "Acceptable use, accounts, plans and billing, availability, termination, and governing law. Also available as Markdown.",
        "responses": { "200": { "$ref": "#/components/responses/ContentPage" } }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": ["content"],
        "operationId": "getLlmsTxt",
        "summary": "Agent guide (llms.txt)",
        "description": "A short Markdown guide for LLM agents: what SpecWatch is for, when to use it, and how to call the public endpoints. Format: llmstxt.org.",
        "responses": {
          "200": {
            "description": "The llms.txt guide.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": ["content"],
        "operationId": "getOpenapi",
        "summary": "This document",
        "description": "The OpenAPI 3.1 description of SpecWatch's public surface.",
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": { "application/json": { "schema": { "type": "object" } } }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "tags": ["content"],
        "operationId": "getRobots",
        "summary": "Robots file",
        "description": "Crawl rules for the site, including the sitemap location.",
        "responses": {
          "200": {
            "description": "The robots.txt file.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "tags": ["content"],
        "operationId": "getSitemap",
        "summary": "Sitemap",
        "description": "XML sitemap of the public pages.",
        "responses": {
          "200": {
            "description": "The sitemap.",
            "content": { "application/xml": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/og.png": {
      "get": {
        "tags": ["content"],
        "operationId": "getOgImage",
        "summary": "Social preview image",
        "description": "The 1200x630 image used for link previews (og:image).",
        "responses": {
          "200": {
            "description": "The preview image.",
            "content": { "image/png": { "schema": { "type": "string", "format": "binary" } } }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ScanRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public http or https URL of the product page, catalog entry, or datasheet to check. Private, loopback, and non-HTTP addresses are rejected."
          }
        },
        "required": ["url"],
        "additionalProperties": false
      },
      "ScanResult": {
        "type": "object",
        "description": "A saved scan of one page.",
        "properties": {
          "id": { "type": "string", "description": "Scan identifier." },
          "url": { "type": "string", "format": "uri", "description": "The URL as requested." },
          "finalUrl": { "type": "string", "format": "uri", "description": "The URL after redirects." },
          "scannedAt": { "type": "string", "format": "date-time", "description": "When the check ran (ISO 8601)." },
          "metadata": { "type": "object", "description": "Page metadata (title, description, canonical if present)." },
          "summary": {
            "type": "object",
            "description": "Headline counts for the scan.",
            "properties": {
              "findings": { "type": "integer", "description": "Number of findings (broken links, spec changes, sync issues)." },
              "linksChecked": { "type": "integer", "description": "Number of outbound links followed." }
            }
          },
          "findings": {
            "type": "array",
            "description": "Per-finding detail: what was found, where, and severity.",
            "items": { "type": "object" }
          },
          "changes": {
            "type": "object",
            "description": "Word-level changes against the page's previous scan, keyed by area that changed. Empty on a first scan."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Human-readable explanation of what went wrong and what to do." }
        },
        "required": ["error"]
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Requests allowed in the window.",
        "schema": { "type": "integer" }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the window.",
        "schema": { "type": "integer" }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets.",
        "schema": { "type": "integer" }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": { "type": "integer" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body was not a usable public http(s) URL.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Back off for Retry-After seconds.",
        "headers": {
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ContentPage": {
        "description": "The page. HTML by default; Markdown with Content-Type text/markdown and Vary: Accept when the request carries Accept: text/markdown.",
        "content": {
          "text/html": { "schema": { "type": "string" } },
          "text/markdown": { "schema": { "type": "string" } }
        }
      }
    }
  }
}
