openapi: "3.0.3"
info:
  title: DetectZeStack API
  description: |
    Tech stack detection + change monitoring API.

    Identify what any website runs (7,300+ technologies: frameworks, CMS,
    CDN, hosting, analytics, security) via HTTP, DNS, TLS, and header
    analysis. Track changes over time, compare competitor stacks, and get
    webhook/email/Slack alerts when a watched site's stack shifts.

    Affordable BuiltWith / Wappalyzer / WhatRuns alternative. Free tier
    included.
  version: "1.0.0"
  contact:
    url: https://detectzestack.com
servers:
  - url: https://detectzestack.com
    description: Production
  - url: https://detectzestack.p.rapidapi.com
    description: RapidAPI Gateway

security:
  - ApiKeyAuth: []
  - RapidAPIAuth: []

paths:
  /health:
    get:
      summary: Health check
      description: Returns the health status of the API, database, and cache.
      security: []
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "503":
          description: Degraded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"

  /analyze:
    get:
      summary: Analyze a URL
      description: |
        Detect the technology stack of a website. Results are cached for 24 hours.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/analyze?url=stripe.com"
        ```

        **Python:**
        ```python
        import requests

        resp = requests.get(
            "https://detectzestack.p.rapidapi.com/analyze",
            params={"url": "stripe.com"},
            headers={
                "X-RapidAPI-Key": "YOUR_KEY",
                "X-RapidAPI-Host": "detectzestack.p.rapidapi.com",
            },
        )
        data = resp.json()
        for tech in data["technologies"]:
            print(f"{tech['name']} ({', '.join(tech['categories'])})")
        ```
      parameters:
        - name: url
          in: query
          required: true
          description: The URL to analyze (e.g., example.com or https://example.com)
          schema:
            type: string
      responses:
        "200":
          description: Analysis result
          headers:
            X-Cache:
              schema:
                type: string
                enum: [HIT, MISS]
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-RateLimit-Used:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyzeResponse"
        "400":
          description: Missing URL parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitError"
        "502":
          description: Failed to analyze URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /analyze/batch:
    post:
      summary: Batch analyze multiple URLs
      description: |
        Analyze up to 10 URLs concurrently. Supports CSV export via Accept header or format query param.

        **Quick start (curl):**
        ```bash
        curl -X POST \
             -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             -H "Content-Type: application/json" \
             -d '{"urls": ["stripe.com", "shopify.com", "github.com"]}' \
             "https://detectzestack.p.rapidapi.com/analyze/batch"
        ```

        **Python:**
        ```python
        import requests

        resp = requests.post(
            "https://detectzestack.p.rapidapi.com/analyze/batch",
            json={"urls": ["stripe.com", "shopify.com", "github.com"]},
            headers={
                "X-RapidAPI-Key": "YOUR_KEY",
                "X-RapidAPI-Host": "detectzestack.p.rapidapi.com",
            },
        )
        for item in resp.json()["results"]:
            techs = item["result"]["technologies"]
            print(f"{item['url']}: {len(techs)} technologies")
        ```
      parameters:
        - name: format
          in: query
          required: false
          description: Response format. Use "csv" for CSV output.
          schema:
            type: string
            enum: [json, csv]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchRequest"
      responses:
        "200":
          description: Batch results (JSON or CSV)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchResponse"
            text/csv:
              schema:
                type: string
                description: "CSV with columns: URL, Domain, Technology, Category, Confidence, Error"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitError"

  /demo:
    get:
      summary: Demo analysis (no auth required)
      description: |
        Try the API without an API key. Analyzes a URL with IP-based rate limiting (20 requests per hour per IP).

        **Try it now (no API key needed):**
        ```bash
        curl "https://detectzestack.com/demo?url=stripe.com"
        ```
      security: []
      parameters:
        - name: url
          in: query
          required: true
          description: The URL to analyze (e.g., example.com or https://example.com)
          schema:
            type: string
      responses:
        "200":
          description: Analysis result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyzeResponse"
        "400":
          description: Missing URL parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Demo rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Failed to analyze URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /compare:
    post:
      summary: Compare technology stacks
      description: |
        Compare the tech stacks of 2-10 websites. Returns shared and unique technologies.

        **Quick start (curl):**
        ```bash
        curl -X POST \
             -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             -H "Content-Type: application/json" \
             -d '{"urls": ["stripe.com", "shopify.com"]}' \
             "https://detectzestack.p.rapidapi.com/compare"
        ```
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompareRequest"
      responses:
        "200":
          description: Comparison result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompareResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitError"

  /history:
    get:
      summary: Get technology history
      description: Retrieve historical technology snapshots for a domain.
      parameters:
        - name: domain
          in: query
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
        - name: include_changes
          in: query
          required: false
          description: When true, each snapshot includes a "changes" array showing what was added/removed/changed compared to the previous snapshot.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: History snapshots
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoryResponse"
        "400":
          description: Missing domain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /stats:
    get:
      summary: Get usage statistics
      description: Returns your API usage stats for the current month.
      responses:
        "200":
          description: Usage statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatsResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /changes:
    get:
      summary: Technology change feed
      description: |
        Get a feed of technology changes detected across scanned domains. Changes are recorded when a domain's tech stack differs from its previous scan.

        History depth is tier-gated:
        - Basic (Free): 7 days
        - Pro ($9/mo): 30 days
        - Ultra ($29/mo): 90 days
        - Mega ($79/mo): 365 days

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/changes?domain=stripe.com"
        ```
      parameters:
        - name: domain
          in: query
          required: false
          description: Filter changes for a specific domain
          schema:
            type: string
      responses:
        "200":
          description: Change feed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeFeedResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks:
    get:
      summary: List webhook subscriptions
      description: List all active webhook subscriptions for your API key.
      responses:
        "200":
          description: Webhook list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookListResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      summary: Create webhook subscription
      description: Subscribe to webhook notifications when a domain is analyzed. The HMAC secret is returned only once at creation time.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookRequest"
      responses:
        "201":
          description: Webhook created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateWebhookResponse"
        "400":
          description: Invalid request or SSRF blocked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks/{id}:
    patch:
      summary: Update webhook subscription
      description: Update a webhook subscription's monitoring interval. Requires paid tier (Pro+).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWebhookRequest"
      responses:
        "200":
          description: Webhook updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: updated
                  monitor_interval:
                    type: string
                    example: daily
        "400":
          description: Invalid interval
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Monitoring requires paid tier
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete webhook subscription
      description: Deactivate a webhook subscription.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: deleted
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /certificate/check:
    get:
      summary: Check TLS certificate
      description: |
        Retrieve TLS certificate details for a domain. Returns TLS version, cipher suite,
        certificate subject/issuer, validity dates, SAN entries, chain of trust, and more.
        Results are cached for 24 hours.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/certificate/check?url=stripe.com"
        ```
      parameters:
        - name: url
          in: query
          required: true
          description: The domain to check (e.g., stripe.com or https://stripe.com)
          schema:
            type: string
      responses:
        "200":
          description: Certificate check result
          headers:
            X-Cache:
              schema:
                type: string
                enum: [HIT, MISS]
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertificateResult"
        "400":
          description: Missing url parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /dns:
    get:
      summary: DNS lookup
      description: |
        Perform a comprehensive DNS lookup for a domain. Returns A, AAAA, CNAME, MX, NS, TXT,
        and PTR records resolved in parallel.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/dns?domain=stripe.com"
        ```
      parameters:
        - name: domain
          in: query
          required: true
          description: The domain to look up (e.g., stripe.com)
          schema:
            type: string
      responses:
        "200":
          description: DNS lookup result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DNSLookupResponse"
        "400":
          description: Missing or invalid domain parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /security:
    get:
      summary: Security headers analysis
      description: |
        Analyze the security headers of a website. Checks HTTPS, HSTS, CSP, X-Frame-Options,
        X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and cookies. Returns a
        letter grade (A+ through F), score, per-header test results, deprecated header warnings,
        information leak detection, and actionable recommendations.
        Results are cached for 24 hours.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/security?url=stripe.com"
        ```
      parameters:
        - name: url
          in: query
          required: true
          description: The URL to analyze (e.g., stripe.com or https://stripe.com)
          schema:
            type: string
      responses:
        "200":
          description: Security headers report
          headers:
            X-Cache:
              schema:
                type: string
                enum: [HIT, MISS]
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecurityReport"
        "400":
          description: Missing url parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Failed to fetch the provided URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /check:
    get:
      summary: Check if a website uses a specific technology
      description: |
        Yes/no check for a single technology on a single site. Returns detection result with
        confidence, version (if detected), and matched categories. Cached for 24 hours per domain.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/check?url=stripe.com&tech=React"
        ```
      parameters:
        - name: url
          in: query
          required: true
          description: The URL to analyze (e.g., stripe.com)
          schema:
            type: string
        - name: tech
          in: query
          required: true
          description: Technology name to check for (case-insensitive, e.g., "React", "Stripe", "Cloudflare")
          schema:
            type: string
      responses:
        "200":
          description: Check result
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: { type: string }
                  technology: { type: string, description: "Canonical tech name if detected, else echoes input" }
                  detected: { type: boolean }
                  confidence: { type: integer, description: "0-100 (0 when not detected)" }
                  version: { type: string }
                  categories: { type: array, items: { type: string } }
                  response_ms: { type: integer }
                  cached: { type: boolean }
        "400":
          description: Missing url or tech parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Failed to fetch the target site
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /lookup:
    get:
      summary: Reverse technology lookup — find sites using a technology
      description: |
        Returns a paginated list of domains where the given technology has been detected across
        the DetectZeStack scan corpus. Useful for technographic targeting, market research, and
        competitive intelligence.

        Result limits are tier-gated: Basic 25 · Pro 100 · Ultra 500 · Mega 2,000.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/lookup?tech=Shopify&limit=50"
        ```
      parameters:
        - name: tech
          in: query
          required: true
          description: Technology name (e.g., "Shopify", "Next.js")
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Max results per page (default 50, clamped to tier max)
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Pagination offset
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Lookup result
          content:
            application/json:
              schema:
                type: object
                properties:
                  technology: { type: string }
                  total: { type: integer, description: "Total matching sites in corpus" }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        domain: { type: string }
                        first_seen: { type: string, format: date-time }
                        last_seen: { type: string, format: date-time }
                  limit: { type: integer }
                  offset: { type: integer }
                  response_ms: { type: integer }
        "400":
          description: Missing tech parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /vulnerability:
    get:
      summary: Vulnerability scan — CVEs for detected technologies
      description: |
        Detects technologies + versions on a site, then queries the NVD (National Vulnerability
        Database) for known CVEs. Returns severity-sorted vulnerabilities with CVSS scores,
        summaries, and reference URLs.

        Only technologies with both a CPE identifier and detected version are scanned. Capped at
        20 NVD lookups per request. Uses the NVD API — this product uses the NVD API but is not
        endorsed or certified by the NVD.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/vulnerability?url=example.com&severity=HIGH"
        ```
      parameters:
        - name: url
          in: query
          required: true
          description: The URL to scan
          schema:
            type: string
        - name: severity
          in: query
          required: false
          description: Filter to a single severity level
          schema:
            type: string
            enum: [CRITICAL, HIGH, MEDIUM, LOW]
      responses:
        "200":
          description: Vulnerability scan result
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: { type: string }
                  scan_date: { type: string, format: date-time }
                  technologies_scanned: { type: integer }
                  technologies_with_cpe: { type: integer }
                  vulnerabilities_found: { type: integer }
                  severity_summary:
                    type: object
                    properties:
                      critical: { type: integer }
                      high: { type: integer }
                      medium: { type: integer }
                      low: { type: integer }
                  vulnerabilities:
                    type: array
                    items:
                      type: object
                      properties:
                        cve_id: { type: string }
                        technology: { type: string }
                        version_detected: { type: string }
                        severity: { type: string }
                        cvss_score: { type: number }
                        summary: { type: string }
                        published_date: { type: string, format: date-time }
                        references: { type: array, items: { type: string } }
                  disclaimer: { type: string }
                  response_ms: { type: integer }
                  cached: { type: boolean }
        "400":
          description: Missing url parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Vulnerability scanning is not configured (server-side)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /site:
    get:
      summary: Site profile — full intelligence in one call (3 credits)
      description: |
        Runs `/analyze` + `/dns` + `/certificate/check` + `/security` in parallel and returns a
        single combined response. Costs **3 credits** against your monthly quota.

        Available on Ultra and Mega tiers only. If all sub-calls fail, credits are refunded.

        **Quick start (curl):**
        ```bash
        curl -H "X-RapidAPI-Key: YOUR_KEY" \
             -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
             "https://detectzestack.p.rapidapi.com/site?url=stripe.com"
        ```
      parameters:
        - name: url
          in: query
          required: true
          description: The URL to profile (e.g., stripe.com)
          schema:
            type: string
      responses:
        "200":
          description: Combined site profile
          headers:
            X-Credits-Used:
              schema:
                type: string
                example: "3"
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: { type: string }
                  url_scanned: { type: string }
                  technologies:
                    type: array
                    items: { $ref: "#/components/schemas/Technology" }
                  categories:
                    type: object
                    additionalProperties:
                      type: array
                      items: { type: string }
                  dns: { $ref: "#/components/schemas/DNSLookupResponse" }
                  certificate: { $ref: "#/components/schemas/CertificateInfo" }
                  security: { $ref: "#/components/schemas/SecurityReport" }
                  meta:
                    type: object
                    description: Page metadata extracted from the HTML head (title, description, og:image, etc.)
                  credits_used: { type: integer, example: 3 }
                  response_ms: { type: integer }
                  cached: { type: boolean }
                  errors:
                    type: array
                    items: { type: string }
                    description: Per-sub-call error messages (partial-success scenarios)
        "400":
          description: Missing url parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Insufficient credits (need 3) or rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /watches:
    get:
      summary: List tech change alert watches
      description: Returns watches owned by the authenticated caller. Slack URLs are masked.
      security:
        - ApiKeyAuth: []
        - RapidAPIAuth: []
      responses:
        "200":
          description: List of watches with quota envelope
          content:
            application/json:
              schema:
                type: object
                properties:
                  watches:
                    type: array
                    items:
                      $ref: "#/components/schemas/Watch"
                  count: { type: integer }
                  limit: { type: integer }
                  tier:
                    type: string
                    enum: [free, basic, pro, business]
        "401":
          description: Authentication required
    post:
      summary: Create a tech change alert watch
      description: Tier-gated. Free (1 weekly), basic (5 daily), pro (25 daily + Slack), business (125 daily + Slack + digest).
      security:
        - ApiKeyAuth: []
        - RapidAPIAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string, example: stripe.com }
                cadence: { type: string, enum: [daily, weekly], default: weekly }
                slack_webhook_url: { type: string, format: uri }
      responses:
        "201":
          description: Watch created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Watch"
        "400":
          description: Invalid domain or RapidAPI subscriber with no email/Slack channel
        "402":
          description: Tier limit or cadence/Slack requires upgrade
        "409":
          description: Already watching this domain

  /watches/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    patch:
      summary: Update a watch
      security:
        - ApiKeyAuth: []
        - RapidAPIAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                active: { type: boolean }
                cadence: { type: string, enum: [daily, weekly] }
                slack_webhook_url:
                  type: string
                  description: Pass empty string to clear the Slack channel.
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Watch"
        "402":
          description: Tier requirement
        "404":
          description: Not found or not owned by caller
    delete:
      summary: Delete a watch
      security:
        - ApiKeyAuth: []
        - RapidAPIAuth: []
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found or not owned by caller

  /watches/email-register:
    post:
      summary: Register an email for alerts (RapidAPI subscribers only)
      description: Sends a 6-digit verification code. Direct-billing customers get 400 because they already have a verified email on file via the users table.
      security:
        - ApiKeyAuth: []
        - RapidAPIAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        "200":
          description: Code sent; expires in 15 minutes
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: code_sent }
                  expires_in_seconds: { type: integer, example: 900 }
        "400":
          description: Direct user, or invalid email
        "429":
          description: Rate limit (5/hour per api_key OR per recipient email)

  /watches/email-verify:
    post:
      summary: Verify the email registration code
      security:
        - ApiKeyAuth: []
        - RapidAPIAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email: { type: string, format: email }
                code: { type: string, pattern: "^[0-9]{6}$" }
      responses:
        "200":
          description: Verified; email_for_alerts updated; pending watches cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: verified }
                  email: { type: string, format: email }
        "401":
          description: Code mismatch, expired, or attempts > 5

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    RapidAPIAuth:
      type: apiKey
      in: header
      name: X-RapidAPI-Proxy-Secret
      description: Provided automatically by RapidAPI gateway

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      required:
        - error

    RateLimitError:
      type: object
      properties:
        error:
          type: string
        limit:
          type: integer
        used:
          type: integer
        tier:
          type: string
        needed:
          type: integer
          description: Number of requests needed for this operation (batch/compare only)
        upgrade:
          type: string
          description: Suggested tier upgrade message

    Technology:
      type: object
      properties:
        name:
          type: string
          example: React
        version:
          type: string
          description: Detected version of the technology (when available)
          example: "18.2.0"
        categories:
          type: array
          items:
            type: string
          example: ["JavaScript frameworks"]
        confidence:
          type: integer
          example: 100
        description:
          type: string
          description: Brief description of the technology (from wappalyzer database)
          example: A JavaScript library for building user interfaces
        website:
          type: string
          description: Official website URL for the technology
          example: https://reactjs.org
        icon:
          type: string
          description: Icon filename from wappalyzer icon set (not a full URL)
          example: React.svg
        cpe:
          type: string
          description: Common Platform Enumeration identifier (when available)
          example: cpe:2.3:a:facebook:react:*:*:*:*:*:*:*:*
        source:
          type: string
          description: |
            Which signal layer surfaced this technology. Useful for filtering
            (e.g. ignore DNS-derived hits if you only trust HTTP evidence) or
            for diagnostics when a detection is unexpected.
          enum: [http, dns, certificate, wappalyzer]
          example: http

    Meta:
      type: object
      properties:
        status_code:
          type: integer
          example: 200
        tech_count:
          type: integer
          example: 12
        scan_depth:
          type: string
          enum: [full, partial]
          example: full
          description: '"full" when HTTP fetch succeeded; "partial" when only DNS-based detection was possible'

    AnalyzeResponse:
      type: object
      properties:
        url:
          type: string
        domain:
          type: string
        technologies:
          type: array
          items:
            $ref: "#/components/schemas/Technology"
        categories:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
        meta:
          $ref: "#/components/schemas/Meta"
        cached:
          type: boolean
        response_ms:
          type: integer
        _usage_warning:
          $ref: "#/components/schemas/UsageWarning"
          description: |
            Present (and only present) when the calling key is approaching or
            has crossed its monthly request limit. Programmatically handle
            this to surface upgrade prompts to your own users before they
            hit a hard 429.

    BatchRequest:
      type: object
      required:
        - urls
      properties:
        urls:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 10

    BatchResultItem:
      type: object
      properties:
        url:
          type: string
        result:
          $ref: "#/components/schemas/AnalyzeResponse"
        error:
          type: string

    BatchResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/BatchResultItem"
        total_ms:
          type: integer
        successful:
          type: integer
        failed:
          type: integer

    CompareRequest:
      type: object
      required:
        - urls
      properties:
        urls:
          type: array
          items:
            type: string
          minItems: 2
          maxItems: 10

    CompareDomain:
      type: object
      properties:
        url:
          type: string
        domain:
          type: string
        technologies:
          type: array
          items:
            $ref: "#/components/schemas/Technology"
        unique:
          type: array
          items:
            type: string
        error:
          type: string

    CompareResponse:
      type: object
      properties:
        domains:
          type: array
          items:
            $ref: "#/components/schemas/CompareDomain"
        shared:
          type: array
          items:
            type: string
        total_ms:
          type: integer

    TechSnapshot:
      type: object
      properties:
        id:
          type: integer
        domain:
          type: string
        technologies:
          type: string
          description: JSON string of technologies array
        status_code:
          type: integer
        tech_count:
          type: integer
        created_at:
          type: string
          format: date-time

    HistoryResponse:
      type: object
      properties:
        domain:
          type: string
        snapshots:
          type: array
          items:
            $ref: "#/components/schemas/TechSnapshot"
        count:
          type: integer
        limit:
          type: integer
        offset:
          type: integer

    StatsResponse:
      type: object
      properties:
        name:
          type: string
        tier:
          type: string
        used:
          type: integer
        limit:
          type: integer
        remaining:
          type: integer
        cache:
          type: object
        top_domains:
          type: array
          description: Your most analyzed domains (top 5)
          items:
            type: object
            properties:
              domain:
                type: string
                example: "github.com"
              count:
                type: integer
                example: 42
        daily_usage:
          type: array
          description: Daily request counts for the last 30 days
          items:
            type: object
            properties:
              date:
                type: string
                example: "2026-02-11"
              count:
                type: integer
                example: 15
        cache_hit_rate:
          type: object
          description: Your cache hit statistics
          properties:
            total:
              type: integer
            cache_hits:
              type: integer
            cache_misses:
              type: integer
            hit_rate:
              type: number
              description: Percentage (0-100)
              example: 65.5
        change_stats:
          $ref: "#/components/schemas/TechChangeStats"

    TechChangeStats:
      type: object
      description: Aggregate statistics about technology changes within your tier's history window
      properties:
        total_changes:
          type: integer
        total_added:
          type: integer
        total_removed:
          type: integer
        total_versioned:
          type: integer
        top_changed:
          type: array
          items:
            type: object
            properties:
              technology:
                type: string
              count:
                type: integer
        recent_changes:
          type: array
          items:
            $ref: "#/components/schemas/TechChange"

    HealthResponse:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded]
        checks:
          type: object
          properties:
            database:
              type: object
            cache:
              type: object
            uptime_seconds:
              type: integer

    WebhookSubscription:
      type: object
      properties:
        id:
          type: integer
        api_key_id:
          type: integer
        domain:
          type: string
        webhook_url:
          type: string
        active:
          type: boolean
        monitor_interval:
          type: string
          description: "Monitoring interval: daily, weekly, or empty (disabled)"
          enum: ["", "daily", "weekly"]
        created_at:
          type: string
          format: date-time

    WebhookListResponse:
      type: object
      properties:
        webhooks:
          type: array
          items:
            $ref: "#/components/schemas/WebhookSubscription"
        count:
          type: integer

    CreateWebhookRequest:
      type: object
      required:
        - domain
        - webhook_url
      properties:
        domain:
          type: string
          example: example.com
        webhook_url:
          type: string
          example: https://your-server.com/webhook
          description: Must be HTTPS. Private/loopback IPs are blocked.

    CreateWebhookResponse:
      type: object
      properties:
        id:
          type: integer
        domain:
          type: string
        webhook_url:
          type: string
        hmac_secret:
          type: string
          description: |
            Save this secret. It is only shown once and used to verify
            webhook signatures.

            Every webhook delivery includes these headers:
              - `X-Webhook-Event:    <event name>`  e.g. `tech_stack.analyzed` or `tech_stack.changed`
              - `X-Webhook-Signature: sha256=<hex>` where `<hex>` is the HMAC-SHA256 of the raw request body, keyed with this `hmac_secret`.

            To verify a delivery:
              1. Compute `expected = "sha256=" + hmac_sha256(secret, raw_body).hex()`
              2. Constant-time-compare `expected` with the value in `X-Webhook-Signature`.
              3. If they don't match, reject the request.

            Worked example: https://detectzestack.com/blog/webhook-tech-change-alerts

    UpdateWebhookRequest:
      type: object
      required:
        - monitor_interval
      properties:
        monitor_interval:
          type: string
          description: "Set monitoring interval: 'daily', 'weekly', or '' to disable"
          enum: ["", "daily", "weekly"]
          example: daily

    WebhookPayload:
      type: object
      description: |
        JSON body sent to webhook subscribers on each analysis.

        Delivery headers (always sent, in addition to the standard ones):
          - `Content-Type: application/json`
          - `X-Webhook-Event: tech_stack.analyzed`
          - `X-Webhook-Signature: sha256=<hex>` (see `hmac_secret` for verification)

        Retry policy: non-2xx responses are retried with exponential backoff,
        up to 3 attempts. After 3 failures the subscription is marked inactive
        and the owner gets an email.
      properties:
        event:
          type: string
          example: tech_stack.analyzed
        domain:
          type: string
        technologies:
          type: array
          items:
            $ref: "#/components/schemas/Technology"
        meta:
          $ref: "#/components/schemas/Meta"
        timestamp:
          type: string
          format: date-time

    MonitorPayload:
      type: object
      description: |
        JSON body sent when a monitored domain's tech stack changes.

        Delivery headers (always sent, in addition to the standard ones):
          - `Content-Type: application/json`
          - `X-Webhook-Event: tech_stack.changed`
          - `X-Webhook-Signature: sha256=<hex>` (see `hmac_secret` for verification)
      properties:
        event:
          type: string
          example: tech_stack.changed
        domain:
          type: string
        changes:
          type: array
          items:
            $ref: "#/components/schemas/Change"
        technologies:
          type: array
          description: Current full technology stack after changes
          items:
            $ref: "#/components/schemas/Technology"
        meta:
          $ref: "#/components/schemas/Meta"
        timestamp:
          type: string
          format: date-time

    TechChange:
      type: object
      description: A recorded technology change for a domain
      properties:
        id:
          type: integer
        domain:
          type: string
          example: stripe.com
        technology:
          type: string
          example: React
        change_type:
          type: string
          enum: [added, removed, version_changed]
          example: added
        category:
          type: string
          example: JavaScript frameworks
        previous_version:
          type: string
          example: "3.6"
        new_version:
          type: string
          example: "18.2"
        snapshot_id:
          type: integer
        created_at:
          type: string
          format: date-time

    ChangeFeedResponse:
      type: object
      properties:
        changes:
          type: array
          items:
            $ref: "#/components/schemas/TechChange"
        count:
          type: integer
        history_days:
          type: integer
          description: Number of days of history available for your tier
        tier:
          type: string
          description: Your current pricing tier

    Change:
      type: object
      description: A single technology change detected during monitoring
      properties:
        type:
          type: string
          enum: [added, removed, version_changed]
          example: added
        technology:
          $ref: "#/components/schemas/Technology"
        previous_version:
          type: string
          description: Previous version (only for version_changed)
          example: "3.6"
        new_version:
          type: string
          description: New version (only for version_changed)
          example: "3.7"

    CertificateResult:
      type: object
      properties:
        domain:
          type: string
          example: stripe.com
        ip:
          type: string
          example: "185.166.216.2"
        port:
          type: integer
          example: 443
        tls:
          $ref: "#/components/schemas/TLSInfo"
        certificate:
          $ref: "#/components/schemas/CertificateInfo"
        chain:
          type: array
          items:
            $ref: "#/components/schemas/ChainEntry"
        has_tls:
          type: boolean
          example: true
        error:
          type: string
          description: Error message when TLS connection fails
        response_ms:
          type: integer
          example: 245

    TLSInfo:
      type: object
      properties:
        version:
          type: string
          example: "TLS 1.3"
        cipher_suite:
          type: string
          example: TLS_AES_128_GCM_SHA256
        negotiated_protocol:
          type: string
          example: h2

    CertificateInfo:
      type: object
      properties:
        subject:
          $ref: "#/components/schemas/SubjectInfo"
        issuer:
          $ref: "#/components/schemas/IssuerInfo"
        serial_number:
          type: string
          example: "04:E1:A2:B3:C4:D5"
        version:
          type: integer
          example: 3
        not_before:
          type: string
          format: date-time
        not_after:
          type: string
          format: date-time
        days_remaining:
          type: integer
          example: 72
        is_expired:
          type: boolean
          example: false
        signature_algorithm:
          type: string
          example: SHA256-RSA
        public_key:
          $ref: "#/components/schemas/PublicKeyInfo"
        san_domains:
          type: array
          items:
            type: string
          example: ["stripe.com", "*.stripe.com"]
        san_ips:
          type: array
          items:
            type: string
        san_emails:
          type: array
          items:
            type: string
        key_usage:
          type: array
          items:
            type: string
          example: ["Digital Signature"]
        ext_key_usage:
          type: array
          items:
            type: string
          example: ["Server Authentication"]
        is_ca:
          type: boolean
          example: false
        ocsp_servers:
          type: array
          items:
            type: string
        issuing_certificate_url:
          type: array
          items:
            type: string
        crl_distribution_points:
          type: array
          items:
            type: string
        subject_key_id:
          type: string
        authority_key_id:
          type: string

    SubjectInfo:
      type: object
      properties:
        common_name:
          type: string
          example: stripe.com
        organization:
          type: array
          items:
            type: string
        country:
          type: array
          items:
            type: string
        province:
          type: array
          items:
            type: string
        locality:
          type: array
          items:
            type: string

    IssuerInfo:
      type: object
      properties:
        common_name:
          type: string
          example: "DigiCert SHA2 Extended Validation Server CA"
        organization:
          type: array
          items:
            type: string
        country:
          type: array
          items:
            type: string

    PublicKeyInfo:
      type: object
      properties:
        algorithm:
          type: string
          example: RSA
        bit_length:
          type: integer
          example: 2048

    ChainEntry:
      type: object
      properties:
        subject:
          type: string
        issuer:
          type: string
        is_ca:
          type: boolean
        not_after:
          type: string
          format: date-time
        signature_algorithm:
          type: string

    DNSLookupResponse:
      type: object
      properties:
        domain:
          type: string
          example: stripe.com
        a:
          type: array
          items:
            type: string
          example: ["185.166.216.2"]
        aaaa:
          type: array
          items:
            type: string
        cname:
          type: string
        mx:
          type: array
          items:
            $ref: "#/components/schemas/MXRecord"
        ns:
          type: array
          items:
            type: string
        txt:
          type: array
          items:
            type: string
        soa:
          $ref: "#/components/schemas/SOARecord"
        ptr:
          type: array
          items:
            type: string
        errors:
          type: array
          description: Non-fatal lookup errors (e.g., NXDOMAIN for a specific record type)
          items:
            type: string
        query_ms:
          type: integer
          example: 42
        response_ms:
          type: integer
          example: 45
        _usage_warning:
          $ref: "#/components/schemas/UsageWarning"

    MXRecord:
      type: object
      properties:
        host:
          type: string
          example: aspmx.l.google.com.
        priority:
          type: integer
          example: 1

    SOARecord:
      type: object
      nullable: true
      description: Start of Authority record (not populated in current version)
      properties:
        primary_ns:
          type: string
        admin_email:
          type: string
        serial:
          type: integer
        refresh:
          type: integer
        retry:
          type: integer
        expire:
          type: integer
        min_ttl:
          type: integer

    UsageWarning:
      type: object
      description: Included when API key usage is approaching its tier limit
      properties:
        message:
          type: string
        upgrade_url:
          type: string

    SecurityReport:
      type: object
      properties:
        url:
          type: string
          example: https://stripe.com
        domain:
          type: string
          example: stripe.com
        grade:
          type: string
          example: A
          description: "Letter grade from A+ to F based on security score"
        score:
          type: integer
          example: 95
        max_score:
          type: integer
          example: 130
        scan_time_ms:
          type: integer
          example: 320
        cached:
          type: boolean
        tests:
          type: object
          description: "Per-header test results keyed by header name (e.g., https, strict-transport-security, content-security-policy)"
          additionalProperties:
            $ref: "#/components/schemas/SecurityTestResult"
        deprecated_headers:
          type: array
          items:
            $ref: "#/components/schemas/DeprecatedHeader"
        info_leaks:
          type: array
          items:
            $ref: "#/components/schemas/InfoLeak"
        recommendations:
          type: array
          items:
            type: string

    SecurityTestResult:
      type: object
      properties:
        pass:
          type: boolean
          example: true
        score_modifier:
          type: integer
          example: 0
        result:
          type: string
          example: hsts-present
        value:
          type: string
          description: Raw header value (when present)
        info:
          type: string
          example: "HSTS header present with max-age of 365 days"
        details:
          description: Additional parsed details (structure varies by test)

    DeprecatedHeader:
      type: object
      properties:
        header:
          type: string
          example: X-XSS-Protection
        value:
          type: string
          example: "1; mode=block"
        warning:
          type: string

    InfoLeak:
      type: object
      properties:
        header:
          type: string
          example: Server
        value:
          type: string
          example: nginx/1.24.0
        warning:
          type: string

    Watch:
      type: object
      properties:
        id:
          type: integer
          format: int64
        domain:
          type: string
          example: stripe.com
        cadence:
          type: string
          enum: [daily, weekly]
        slack_webhook_url:
          type: string
          description: Masked; only the last 4 chars of the secret token are revealed.
          example: "https://hooks.slack.com/services/T0/B0/…ZW90"
        active:
          type: boolean
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        last_known_snapshot_id:
          type: integer
          nullable: true
        consecutive_failures:
          type: integer
        consecutive_slack_failures:
          type: integer
        consecutive_email_failures:
          type: integer
        last_failure_kind:
          type: string
          enum: ["", transient, permanent]
        pending_email_verification:
          type: boolean
        created_at:
          type: string
          format: date-time
