openapi: 3.1.0
info:
  title: SnapAPI — Web Intelligence API
  version: 1.5.0
  description: |
    SnapAPI is a web intelligence API that understands, captures, and monitors any webpage.
    One call to /v1/analyze returns page type, primary CTA, navigation, forms, technologies,
    word count, load time, OG metadata, and an optional visual snapshot — all from a single
    browser session. Also supports standalone visual snapshots (PNG/JPEG/WebP), metadata
    extraction, HTML-to-image rendering, and structural monitors with pixel and DOM diffing.

    ## Authentication

    Authenticate using any one of the following methods:
    - **Header**: `x-api-key: <your-key>`
    - **Query parameter**: `?api_key=<your-key>`
    - **Bearer token**: `Authorization: Bearer <your-key>`

    ## Rate Limits & Tiers

    | Tier     | Monthly Limit | Concurrent | Monitors | Min Interval | History |
    |----------|--------------|------------|----------|-------------|---------|
    | free     | 100          | 1          | 0        | —           | —       |
    | starter  | 1,000        | 2          | 3        | 30 min      | 3 days  |
    | pro      | 5,000        | 3          | 20       | 5 min       | 30 days |
    | business | 25,000       | 5          | 100      | 1 min       | 90 days |

    Usage headers are returned on API responses:
    - `X-Cache`: HIT or MISS
    - `X-Usage-Count`: requests used this month
    - `X-Usage-Limit`: monthly limit for your tier
  contact:
    name: SnapAPI Support
    url: https://snapapi.tech
  license:
    name: Proprietary

servers:
  - url: https://snapapi.tech
    description: Production

security:
  - ApiKeyHeader: []
  - ApiKeyQuery: []
  - BearerAuth: []

tags:
  - name: Screenshot
    description: Capture web page screenshots
  - name: Metadata
    description: Extract page metadata without taking a screenshot
  - name: Render
    description: Render raw HTML to an image
  - name: Monitors
    description: Scheduled screenshot monitoring with visual change detection
  - name: Visual Diff
    description: Pixel-level visual regression testing — compare screenshots, detect layout changes
  - name: Account
    description: Account management and recovery
  - name: Health
    description: Server health check

paths:
  /v1/screenshot:
    get:
      operationId: takeScreenshot
      tags: [Screenshot]
      summary: Capture a screenshot of a web page
      description: |
        Renders the target URL in a headless browser and returns the screenshot
        as a binary image. When `meta=true`, returns a JSON object containing
        the screenshot as a base64 data URI along with page metadata.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
        - BearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: The URL of the page to capture.
          schema:
            type: string
            format: uri
          example: https://example.com
        - name: width
          in: query
          description: Viewport width in pixels.
          schema:
            type: integer
            default: 1280
            minimum: 1
          example: 1920
        - name: height
          in: query
          description: Viewport height in pixels.
          schema:
            type: integer
            default: 800
            minimum: 1
          example: 1080
        - name: format
          in: query
          description: Image output format.
          schema:
            type: string
            enum: [png, jpeg, webp]
            default: png
          example: png
        - name: quality
          in: query
          description: Image quality (applies to JPEG and WebP).
          schema:
            type: integer
            default: 80
            minimum: 1
            maximum: 100
          example: 90
        - name: full_page
          in: query
          description: Capture the full scrollable page instead of just the viewport.
          schema:
            type: boolean
            default: false
          example: true
        - name: delay
          in: query
          description: Milliseconds to wait after page load before capturing.
          schema:
            type: integer
            default: 0
            minimum: 0
            maximum: 10000
          example: 2000
        - name: dark_mode
          in: query
          description: "Emulate dark color scheme via prefers-color-scheme: dark."
          schema:
            type: boolean
            default: false
          example: true
        - name: block_ads
          in: query
          description: "Block common ad networks and trackers during rendering."
          schema:
            type: boolean
            default: false
          example: true
        - name: cache
          in: query
          description: Use cached screenshot if available.
          schema:
            type: boolean
            default: true
          example: false
        - name: selector
          in: query
          description: CSS selector to capture a specific element instead of the full page.
          schema:
            type: string
          example: "#main-content"
        - name: device
          in: query
          description: "Device emulation preset. Overrides width/height and sets a mobile user agent. Available presets: iphone14, iphone14pro, iphone15, pixel7, pixel8, galaxys23, ipad, ipadpro, desktop."
          schema:
            type: string
            enum: [iphone14, iphone14pro, iphone15, pixel7, pixel8, galaxys23, ipad, ipadpro, desktop]
          example: iphone14
        - name: meta
          in: query
          description: When true, returns JSON with page metadata and a base64 screenshot instead of a binary image.
          schema:
            type: boolean
            default: false
          example: true
      responses:
        "200":
          description: Screenshot captured successfully.
          headers:
            X-Cache:
              description: Whether the response was served from cache.
              schema:
                type: string
                enum: [HIT, MISS]
            X-Usage-Count:
              description: Number of requests used this billing month.
              schema:
                type: integer
            X-Usage-Limit:
              description: Monthly request limit for your tier.
              schema:
                type: integer
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                $ref: "#/components/schemas/MetaResponse"
              example:
                title: Example Domain
                description: This domain is for use in illustrative examples.
                favicon: https://example.com/favicon.ico
                language: en
                headings:
                  - level: 1
                    text: Example Domain
                links:
                  - text: "More information..."
                    href: "https://www.iana.org/domains/example"
                text: "Example Domain\nThis domain is for use in illustrative examples in documents."
                url: https://example.com
                screenshot: "data:image/png;base64,iVBORw0KGgo..."
                format: png
                width: 1280
                height: 800
        "400":
          description: Bad request — invalid URL, blocked site, or selector not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Invalid URL provided"
        "401":
          description: No API key provided.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "API key required"
        "403":
          description: Invalid or revoked API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Invalid API key"
        "429":
          description: Rate limit exceeded or concurrent request limit reached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Monthly usage limit exceeded"
        "503":
          description: Server busy — all browser slots occupied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Server busy, try again later"

  /v1/metadata:
    get:
      operationId: getMetadata
      tags: [Metadata]
      summary: Extract metadata from a URL
      description: |
        Fast metadata extraction without taking a screenshot. Returns title,
        description, OG tags, favicon, canonical URL, language, headings, and links.
        Available on all plans. Much faster than `screenshot?meta=true` because it
        skips image rendering entirely.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
        - BearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: The URL of the page to extract metadata from.
          schema:
            type: string
            format: uri
          example: https://example.com
      responses:
        "200":
          description: Metadata extracted successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetadataResult"
              example:
                url: https://example.com
                title: Example Domain
                description: This domain is for use in illustrative examples.
                og_title: ""
                og_image: ""
                og_type: ""
                favicon: https://example.com/favicon.ico
                viewport: "width=device-width, initial-scale=1"
                canonical: https://example.com
                language: en
                headings:
                  - level: 1
                    text: Example Domain
                links:
                  - text: "More information..."
                    href: "https://www.iana.org/domains/example"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/analyze:
    get:
      operationId: analyzePage
      tags: [Analyze]
      summary: Analyze a webpage structure
      description: |
        Returns structured understanding of any webpage: page type, primary CTA,
        navigation items, buttons, forms, headings, images, links, word count,
        load time in ms, and detected technologies (React, Stripe, GTM, Shopify, etc.).
        DOM-based analysis — no AI processing. Available on all plans.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
        - BearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: The URL of the page to analyze.
          schema:
            type: string
            format: uri
          example: https://stripe.com
        - name: screenshot
          in: query
          required: false
          description: Include a base64 PNG screenshot in the response.
          schema:
            type: boolean
            default: false
        - name: wait_for_selector
          in: query
          required: false
          description: CSS selector to wait for before analyzing. Essential for SPAs and React apps.
          schema:
            type: string
          example: "#app"
        - name: wait_for_network_idle
          in: query
          required: false
          description: Wait for all network requests to complete after page load.
          schema:
            type: boolean
            default: false
        - name: delay
          in: query
          required: false
          description: Milliseconds to wait after page load before analyzing (max 10000).
          schema:
            type: integer
            default: 0
        - name: interact
          in: query
          required: false
          description: |
            JSON array of browser actions to perform before analysis. Supports:
            `click` (requires selector), `type` (requires selector + value),
            `hover` (requires selector), `wait` (requires ms, max 5000),
            `scroll_to` (optional selector, defaults to page bottom).
            Max 20 steps. Each step returns 400 on failure.
          schema:
            type: string
          example: '[{"action":"click","selector":"#tab-pricing"},{"action":"wait","ms":500}]'
      responses:
        "200":
          description: Page analyzed successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyzeResult"
              example:
                url: https://stripe.com
                load_time_ms: 980
                title: "Stripe | Financial Infrastructure for the Internet"
                description: "Stripe powers online and in-person payment processing..."
                page_type: landing
                primary_cta:
                  text: Start now
                  href: https://stripe.com/register
                nav_items:
                  - text: Products
                    href: https://stripe.com/products
                buttons:
                  - text: Start now
                    href: https://stripe.com/register
                forms: []
                headings:
                  - level: 1
                    text: Financial infrastructure for the internet
                images:
                  - src: https://stripe.com/img/hero.png
                    alt: Stripe dashboard
                    width: 800
                    height: 600
                links:
                  - text: Pricing
                    href: https://stripe.com/pricing
                    external: false
                word_count: 1432
                technologies:
                  - react
                  - stripe
                  - google-analytics
                text_summary: "Stripe powers online and in-person payment processing and financial solutions..."
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/pdf:
    get:
      operationId: generatePdf
      summary: Convert any webpage to PDF
      tags: [Capture]
      parameters:
        - { name: url, in: query, required: true, schema: { type: string }, description: "URL to convert" }
        - { name: format, in: query, schema: { type: string, enum: [A3, A4, A5, Letter, Legal, Tabloid], default: A4 }, description: "Paper format" }
        - { name: landscape, in: query, schema: { type: boolean, default: false }, description: "Landscape orientation" }
        - { name: print_background, in: query, schema: { type: boolean, default: true }, description: "Include background colors/images" }
        - { name: scale, in: query, schema: { type: number, minimum: 0.1, maximum: 2, default: 1 }, description: "Page scale factor" }
        - { name: margin_top, in: query, schema: { type: integer, default: 20 }, description: "Top margin in px" }
        - { name: margin_right, in: query, schema: { type: integer, default: 20 }, description: "Right margin in px" }
        - { name: margin_bottom, in: query, schema: { type: integer, default: 20 }, description: "Bottom margin in px" }
        - { name: margin_left, in: query, schema: { type: integer, default: 20 }, description: "Left margin in px" }
        - { name: delay, in: query, schema: { type: integer, default: 0, maximum: 10000 }, description: "Wait ms after load" }
      responses:
        "200":
          description: PDF file
          content:
            application/pdf:
              schema: { type: string, format: binary }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/render:
    post:
      operationId: renderHtml
      tags: [Render]
      summary: Render raw HTML to an image
      description: |
        Accepts a full HTML string and renders it in a headless browser, returning
        the result as a binary image. Perfect for generating OG images, email
        previews, report cards, and dashboard exports programmatically.
        Requires Starter tier or above.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [html]
              properties:
                html:
                  type: string
                  description: Full HTML string to render. Maximum 2MB.
                  example: "<html><body style='background:#0f172a;color:#fff;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh'><h1>Hello World</h1></body></html>"
                width:
                  type: integer
                  description: Viewport width in pixels.
                  default: 1200
                  example: 1200
                height:
                  type: integer
                  description: Viewport height in pixels.
                  default: 630
                  example: 630
                format:
                  type: string
                  enum: [png, jpeg, webp]
                  default: png
                  description: Output image format.
                quality:
                  type: integer
                  minimum: 1
                  maximum: 100
                  default: 90
                  description: Image quality for jpeg/webp.
      responses:
        "200":
          description: HTML rendered to image successfully.
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Free tier — render requires Starter or above.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/batch:
    post:
      operationId: batchProcess
      tags: [Batch]
      summary: Process multiple URLs in one request
      description: |
        Accepts a list of URLs and calls the specified endpoint (analyze, screenshot, or metadata)
        for each one concurrently. Each URL counts as one API call against your monthly quota.
        Batch limits by tier: Starter 10, Pro 25, Business 50. Free tier not eligible.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [urls]
              properties:
                urls:
                  type: array
                  items:
                    type: string
                    format: uri
                  description: Array of URLs to process.
                  example: ["https://stripe.com", "https://github.com"]
                endpoint:
                  type: string
                  enum: [analyze, screenshot, metadata]
                  default: analyze
                  description: Which endpoint to call for each URL.
                params:
                  type: object
                  additionalProperties:
                    type: string
                  description: Query parameters forwarded to each endpoint call.
                  example: { "screenshot": "false" }
      responses:
        "200":
          description: Batch results — one entry per URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        url:
                          type: string
                        status:
                          type: string
                          enum: [ok, error]
                        data:
                          type: object
                          description: Endpoint response (present when status is ok)
                        error:
                          type: string
                          description: Error message (present when status is error)
                  total:
                    type: integer
                  succeeded:
                    type: integer
                  failed:
                    type: integer
                  duration_ms:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Free tier — batch requires Starter or above.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/usage:
    get:
      operationId: getUsage
      tags: [Account]
      summary: Get current usage statistics
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
        - BearerAuth: []
      responses:
        "200":
          description: Current usage data.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
              example:
                tier: free
                usage:
                  month: "2026-03"
                  count: 42
                limits:
                  monthly: 100
                  concurrent: 1
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/account:
    get:
      operationId: getAccount
      tags: [Account]
      summary: Get account details
      description: Returns full account information including tier, usage, permissions, and domain restrictions.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      responses:
        "200":
          description: Account details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountResponse"
              example:
                name: Andrew
                email: user@example.com
                tier: starter
                usage:
                  month: "2026-03"
                  count: 150
                  limit: 1000
                concurrent: 2
                created: "2026-01-15T08:30:00Z"
                allowed_domains: ["example.com", "mysite.dev"]
                permissions: ["screenshot", "meta"]
                manage_subscription: "https://billing.stripe.com/session/abc123"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/account/settings:
    post:
      operationId: updateAccountSettings
      tags: [Account]
      summary: Update account settings
      description: Update display name and allowed domain restrictions.
      security:
        - ApiKeyHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Display name for the account.
                  example: Andrew
                allowed_domains:
                  type: array
                  items:
                    type: string
                  description: List of domains allowed to use this API key. Empty array means no restriction.
                  example: ["example.com", "mysite.dev"]
      responses:
        "200":
          description: Settings updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: boolean
                required: [updated]
              example:
                updated: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/account/recover:
    post:
      operationId: recoverAccount
      tags: [Account]
      summary: Send account recovery code
      description: Sends a verification code to the email address on file. Use `/v1/account/verify` to complete recovery.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  description: The email address associated with the account.
                  example: user@example.com
      responses:
        "200":
          description: Recovery code sent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sent:
                    type: boolean
                  message:
                    type: string
                required: [sent, message]
              example:
                sent: true
                message: "Recovery code sent to your email"

  /v1/account/verify:
    post:
      operationId: verifyAccount
      tags: [Account]
      summary: Verify recovery code and retrieve API key
      description: Completes account recovery by verifying the code sent via `/v1/account/recover`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email:
                  type: string
                  format: email
                  example: user@example.com
                code:
                  type: string
                  description: The verification code received by email.
                  example: "482901"
      responses:
        "200":
          description: Verification successful. Returns the API key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    type: string
                    description: The recovered API key.
                  tier:
                    type: string
                    enum: [free, starter, pro, business]
                required: [key, tier]
              example:
                key: "snap_k1a2b3c4d5e6f7..."
                tier: free

  /v1/account/regenerate:
    post:
      operationId: regenerateApiKey
      tags: [Account]
      summary: Regenerate API key
      description: Invalidates the current API key and issues a new one. Authenticate via header or request body.
      security:
        - ApiKeyHeader: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                  description: Current API key (alternative to header auth).
      responses:
        "200":
          description: New API key generated. The old key is immediately revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    type: string
                    description: The new API key.
                  message:
                    type: string
                required: [key, message]
              example:
                key: "snap_n8m7l6k5j4h3g2..."
                message: "API key regenerated. Old key is now invalid."
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/signup:
    post:
      operationId: signup
      tags: [Account]
      summary: Create a new account
      description: |
        Registers a new free-tier account. If the email already exists, returns
        a masked version of the existing key instead of creating a duplicate.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: newuser@example.com
      responses:
        "200":
          description: Account created or already exists.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/SignupSuccess"
                  - $ref: "#/components/schemas/SignupExists"
              examples:
                new_account:
                  summary: New account created
                  value:
                    key: "snap_a1b2c3d4e5f6g7..."
                    tier: free
                    limit: 100
                    message: "Account created. Save your API key — it won't be shown again."
                existing_account:
                  summary: Account already exists
                  value:
                    exists: true
                    masked_key: "snap_a1b2...g7h8"
                    tier: free
                    message: "Account already exists. Use /v1/account/recover to retrieve your key."

  /v1/monitors:
    post:
      operationId: createMonitor
      tags: [Monitors]
      summary: Create a scheduled monitor
      description: |
        Creates a new monitor that captures screenshots on a recurring schedule.
        Available on Starter plans and above. Each capture counts toward monthly usage.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: URL to monitor.
                  example: https://example.com
                name:
                  type: string
                  description: Display name (defaults to hostname).
                  example: My Site
                interval:
                  type: integer
                  description: Minutes between screenshots. Minimum depends on tier.
                  default: 60
                  example: 30
                webhook_url:
                  type: string
                  format: uri
                  description: URL to POST when a screenshot is captured.
                params:
                  type: object
                  description: Screenshot and analysis parameters.
                  properties:
                    width: { type: integer, default: 1280 }
                    height: { type: integer, default: 800 }
                    format: { type: string, enum: [png, jpeg, webp], default: png }
                    quality: { type: integer, default: 80 }
                    full_page: { type: boolean, default: false }
                    dark_mode: { type: boolean, default: false }
                    selector: { type: string, description: CSS selector to capture }
                    delay: { type: integer, default: 0, description: Ms to wait after load }
                    analyze:
                      type: boolean
                      default: false
                      description: Run structural analysis on each snapshot. Detects CTA changes, technology additions/removals, H1 rewrites, word count shifts, and more. Results appear in snapshot meta and webhook payload as structural_changes.
      responses:
        "200":
          description: Monitor created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          description: Free tier or monitor limit reached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    get:
      operationId: listMonitors
      tags: [Monitors]
      summary: List all monitors
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      responses:
        "200":
          description: List of monitors with tier limits.
          content:
            application/json:
              schema:
                type: object
                properties:
                  monitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Monitor"
                  limits:
                    $ref: "#/components/schemas/MonitorLimits"

  /v1/monitors/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Monitor ID.
    get:
      operationId: getMonitor
      tags: [Monitors]
      summary: Get monitor details with recent snapshots
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      responses:
        "200":
          description: Monitor with snapshot history.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Monitor"
                  - type: object
                    properties:
                      snapshots:
                        type: array
                        items:
                          $ref: "#/components/schemas/Snapshot"
        "404":
          description: Monitor not found.
    patch:
      operationId: updateMonitor
      tags: [Monitors]
      summary: Update a monitor
      description: Pause/resume, change interval, rename, or update webhook.
      security:
        - ApiKeyHeader: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                interval:
                  type: integer
                status:
                  type: string
                  enum: [active, paused]
                webhook_url:
                  type: string
                params:
                  type: object
      responses:
        "200":
          description: Monitor updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: boolean
                  id:
                    type: string
        "404":
          description: Monitor not found.
    delete:
      operationId: deleteMonitor
      tags: [Monitors]
      summary: Delete a monitor and all its snapshots
      security:
        - ApiKeyHeader: []
      responses:
        "200":
          description: Monitor deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                  id:
                    type: string
        "404":
          description: Monitor not found.

  /v1/monitors/{id}/snapshots:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listSnapshots
      tags: [Monitors]
      summary: List snapshot history for a monitor
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: Paginated snapshot list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  snapshots:
                    type: array
                    items:
                      $ref: "#/components/schemas/Snapshot"
                  pagination:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      pages:
                        type: integer

  /v1/monitors/{id}/snapshots/{sid}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: sid
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getSnapshotImage
      tags: [Monitors]
      summary: Get a snapshot image
      description: Returns the actual screenshot image file.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      responses:
        "200":
          description: Screenshot image.
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
        "404":
          description: Snapshot not found.
        "410":
          description: Snapshot expired or failed.

  /v1/visual-diff:
    post:
      operationId: visualDiff
      tags: [Visual Diff]
      summary: Compare webpage screenshots pixel-by-pixel
      description: |
        Captures a screenshot of the target URL and compares it against either a previously
        stored baseline (compare_to: "last_capture") or a second URL (compare_to: "baseline_url").

        Returns the number of changed pixels, a diff image with changed regions highlighted in red,
        bounding boxes of changed regions, and a layout shift score.

        First call with compare_to: "last_capture" stores the baseline and returns changed: false.
        Subsequent calls compare against the stored baseline and update it.
      security:
        - ApiKeyHeader: []
        - ApiKeyQuery: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  description: URL of the page to capture and compare.
                  example: "https://example.com"
                compare_to:
                  type: string
                  enum: [last_capture, baseline_url]
                  default: last_capture
                  description: |
                    "last_capture" — compare against the last screenshot taken for this URL.
                    "baseline_url" — compare against a screenshot of baseline_url.
                baseline_url:
                  type: string
                  description: Required when compare_to is "baseline_url". URL to use as the baseline.
                  example: "https://production.example.com"
                width:
                  type: integer
                  default: 1280
                  description: Viewport width in pixels (320–3840).
                height:
                  type: integer
                  default: 800
                  description: Viewport height in pixels (200–2160).
      responses:
        "200":
          description: Diff result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  changed:
                    type: boolean
                    description: True if any pixels differ between the two screenshots.
                  changed_pixels:
                    type: integer
                    description: Number of pixels that differ.
                  diff_image:
                    type: string
                    nullable: true
                    description: |
                      Base64-encoded PNG (data:image/png;base64,...) with changed pixels
                      highlighted in red. Null when changed is false.
                  changed_regions:
                    type: array
                    description: Bounding boxes of changed regions.
                    items:
                      type: object
                      properties:
                        x: { type: integer }
                        y: { type: integer }
                        width: { type: integer }
                        height: { type: integer }
                  layout_shift:
                    type: number
                    description: |
                      Fraction of total pixels that changed (0.0 = identical, 1.0 = fully different).
                      Use as a threshold in CI/CD pipelines (e.g. fail if > 0.05).
                  baseline_captured_at:
                    type: string
                    nullable: true
                    description: ISO timestamp of when the baseline was captured (last_capture mode only).
                  message:
                    type: string
                    description: Present on first call when no baseline existed yet.
        "400":
          description: Missing or invalid parameters.
        "401":
          description: Missing API key.
        "403":
          description: Invalid API key.
        "503":
          description: Server busy — retry in a few seconds.

  /v1/tool:
    get:
      operationId: getToolDefinition
      tags: [Health]
      summary: Get function calling tool definition
      description: "Returns a JSON function schema compatible with OpenAI, Claude, and LangChain tool definitions. Use this to auto-register SnapAPI as an agent tool."
      security: []
      responses:
        "200":
          description: Tool definition for function calling.
          content:
            application/json:
              schema:
                type: object

  /health:
    get:
      operationId: healthCheck
      tags: [Health]
      summary: Server health check
      description: Returns server status, active job count, and uptime. No authentication required.
      security: []
      responses:
        "200":
          description: Server is healthy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  activeJobs:
                    type: integer
                    description: Number of screenshots currently being processed.
                  uptime:
                    type: number
                    description: Server uptime in seconds.
                required: [status, activeJobs, uptime]
              example:
                status: ok
                activeJobs: 2
                uptime: 86423.7

components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: API key passed via the `x-api-key` request header.
    ApiKeyQuery:
      type: apiKey
      in: query
      name: api_key
      description: API key passed as a query parameter.
    BearerAuth:
      type: http
      scheme: bearer
      description: API key passed as a Bearer token in the `Authorization` header.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
      required: [error]

    AnalyzeResult:
      type: object
      description: Structured analysis of a webpage.
      properties:
        url:
          type: string
          format: uri
        load_time_ms:
          type: integer
          description: Time to DOM ready in milliseconds.
        title:
          type: string
        description:
          type: string
        page_type:
          type: string
          enum: [landing, blog, ecommerce, docs, dashboard, login, other]
          description: Detected page type based on DOM heuristics.
        primary_cta:
          type: object
          nullable: true
          description: Most prominent call-to-action found on the page.
          properties:
            text:
              type: string
            href:
              type: string
              format: uri
              nullable: true
        nav_items:
          type: array
          items:
            type: object
            properties:
              text:
                type: string
              href:
                type: string
        buttons:
          type: array
          items:
            type: object
            properties:
              text:
                type: string
              href:
                type: string
                nullable: true
        forms:
          type: array
          items:
            type: object
            properties:
              action:
                type: string
                nullable: true
              method:
                type: string
              fields:
                type: array
                items:
                  type: string
              submit_text:
                type: string
        headings:
          type: array
          items:
            type: object
            properties:
              level:
                type: integer
                enum: [1, 2, 3, 4]
              text:
                type: string
        images:
          type: array
          items:
            type: object
            properties:
              src:
                type: string
              alt:
                type: string
              width:
                type: integer
                nullable: true
              height:
                type: integer
                nullable: true
        links:
          type: array
          items:
            type: object
            properties:
              text:
                type: string
              href:
                type: string
              external:
                type: boolean
        word_count:
          type: integer
        technologies:
          type: array
          items:
            type: string
          description: Detected libraries and tools (e.g. "react", "stripe", "google-analytics").
        text_summary:
          type: string
          description: First meaningful paragraph of visible page text.
        screenshot:
          type: string
          nullable: true
          description: Base64-encoded PNG (only present when screenshot=true was requested).

    MetadataResult:
      type: object
      description: Structured metadata extracted from a webpage.
      properties:
        url:
          type: string
          format: uri
          description: Final URL after any redirects.
        title:
          type: string
          description: Page title from the `<title>` tag.
        description:
          type: string
          description: Meta description content.
        og_title:
          type: string
          description: Open Graph title.
        og_image:
          type: string
          format: uri
          description: Open Graph image URL.
        og_type:
          type: string
          description: Open Graph type (e.g. "website", "article").
        favicon:
          type: string
          format: uri
          description: Absolute URL of the page favicon.
        viewport:
          type: string
          description: Viewport meta tag content.
        canonical:
          type: string
          format: uri
          description: Canonical URL.
        language:
          type: string
          description: Page language from the `lang` attribute (e.g. "en").
        headings:
          type: array
          description: Headings (h1-h3) extracted from the page.
          items:
            type: object
            properties:
              level:
                type: integer
                enum: [1, 2, 3]
              text:
                type: string
            required: [level, text]
        links:
          type: array
          description: Links with text and href.
          items:
            type: object
            properties:
              text:
                type: string
              href:
                type: string
                format: uri
            required: [text, href]
      required: [url, title]

    MetaResponse:
      type: object
      description: Returned when `meta=true` on the screenshot endpoint. Includes structured page data for AI agent consumption.
      properties:
        title:
          type: string
          description: Page title from the `<title>` tag.
        description:
          type: string
          description: Page description from the meta description tag.
        favicon:
          type: string
          format: uri
          description: Absolute URL of the page favicon.
        language:
          type: string
          description: Page language from the `lang` attribute (e.g. "en").
        headings:
          type: array
          description: First 20 headings (h1-h3) extracted from the page.
          items:
            type: object
            properties:
              level:
                type: integer
                enum: [1, 2, 3]
              text:
                type: string
            required: [level, text]
        links:
          type: array
          description: First 50 links with text and href.
          items:
            type: object
            properties:
              text:
                type: string
              href:
                type: string
                format: uri
            required: [text, href]
        text:
          type: string
          description: Extracted visible text content (first 2000 characters).
        url:
          type: string
          format: uri
          description: Final URL after any redirects.
        screenshot:
          type: string
          description: Base64-encoded data URI of the screenshot image.
        format:
          type: string
          enum: [png, jpeg, webp]
        width:
          type: integer
        height:
          type: integer
      required: [title, url, screenshot, format, width, height]

    UsageResponse:
      type: object
      properties:
        tier:
          type: string
          enum: [free, starter, pro, business]
        usage:
          type: object
          properties:
            month:
              type: string
              description: Current billing month (YYYY-MM).
            count:
              type: integer
              description: Requests used this month.
          required: [month, count]
        limits:
          type: object
          properties:
            monthly:
              type: integer
              description: Maximum requests per month.
            concurrent:
              type: integer
              description: Maximum concurrent screenshot jobs.
          required: [monthly, concurrent]
        manage_subscription:
          type: string
          format: uri
          description: Stripe billing portal URL (present for paid tiers).
      required: [tier, usage, limits]

    AccountResponse:
      type: object
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        tier:
          type: string
          enum: [free, starter, pro, business]
        usage:
          type: object
          properties:
            month:
              type: string
            count:
              type: integer
            limit:
              type: integer
          required: [month, count, limit]
        concurrent:
          type: integer
        monitors:
          type: object
          description: Monitor tier limits.
          properties:
            max:
              type: integer
            min_interval:
              type: integer
            history_days:
              type: integer
        created:
          type: string
          format: date-time
        allowed_domains:
          type: array
          items:
            type: string
        permissions:
          type: array
          items:
            type: string
        manage_subscription:
          type: string
          format: uri
          description: Billing portal URL (present for paid tiers).
      required: [name, email, tier, usage, concurrent, monitors, created, permissions]

    SignupSuccess:
      type: object
      properties:
        key:
          type: string
          description: Your new API key. Save it — it will not be shown again.
        tier:
          type: string
          enum: [free, starter, pro, business]
        limit:
          type: integer
          description: Monthly request limit for your tier.
        message:
          type: string
      required: [key, tier, limit, message]

    SignupExists:
      type: object
      properties:
        exists:
          type: boolean
          const: true
        masked_key:
          type: string
          description: Partially masked version of the existing API key.
        tier:
          type: string
          enum: [free, starter, pro, business]
        message:
          type: string
      required: [exists, masked_key, tier, message]

    Monitor:
      type: object
      properties:
        id:
          type: string
          example: mon_a1b2c3d4e5f6g7h8
        name:
          type: string
          example: My Site
        url:
          type: string
          format: uri
        interval:
          type: integer
          description: Minutes between screenshots.
        status:
          type: string
          enum: [active, paused]
        params:
          type: object
        webhook_url:
          type: string
          format: uri
        created:
          type: string
          format: date-time
        last_run:
          type: string
          format: date-time
        next_run:
          type: string
          format: date-time
        run_count:
          type: integer
        error_count:
          type: integer
        last_error:
          type: string
      required: [id, name, url, interval, status]

    Snapshot:
      type: object
      properties:
        id:
          type: string
          example: snap_x9y8z7w6v5u4
        taken_at:
          type: string
          format: date-time
        file_size:
          type: integer
          description: File size in bytes.
        status:
          type: string
          enum: [ok, error]
        error:
          type: string
        meta:
          type: object
          properties:
            title:
              type: string
            description:
              type: string
        image_url:
          type: string
          description: Relative URL to fetch the screenshot image.
        changed:
          type: boolean
          description: True on any visual or structural change since the previous snapshot.
        diff_percent:
          type: number
          format: float
          description: Percentage of pixels that changed (0–100). 0 on first capture.
        structural_changes:
          type: object
          nullable: true
          description: Present only when analyze=true and something structurally changed. Keys only appear when that field changed.
          properties:
            primary_cta:
              type: object
              properties:
                before: { type: string, nullable: true }
                after: { type: string, nullable: true }
            page_type:
              type: object
              properties:
                before: { type: string }
                after: { type: string }
            h1:
              type: object
              properties:
                before: { type: string }
                after: { type: string }
            og_title:
              type: object
              properties:
                before: { type: string }
                after: { type: string }
            technologies:
              type: object
              properties:
                added: { type: array, items: { type: string } }
                removed: { type: array, items: { type: string } }
            word_count:
              type: object
              properties:
                before: { type: integer }
                after: { type: integer }
            forms_count:
              type: object
              properties:
                before: { type: integer }
                after: { type: integer }
        analysis:
          type: object
          nullable: true
          description: Full structural analysis for this snapshot. Only present when analyze=true.
          properties:
            page_type: { type: string }
            primary_cta: { type: object, nullable: true }
            h1: { type: string }
            technologies: { type: array, items: { type: string } }
            word_count: { type: integer }
            forms_count: { type: integer }
            og_title: { type: string }
            og_image: { type: string }
      required: [id, taken_at, status, image_url]

    MonitorLimits:
      type: object
      properties:
        max:
          type: integer
          description: Maximum monitors for this tier.
        used:
          type: integer
          description: Current number of active monitors.
        min_interval:
          type: integer
          description: Minimum interval in minutes for this tier.
        history_days:
          type: integer
          description: Number of days snapshots are retained.
      required: [max, used, min_interval, history_days]

  responses:
    Unauthorized:
      description: No API key provided.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "API key required"
    Forbidden:
      description: Invalid or revoked API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Invalid API key"
    BadRequest:
      description: Bad request — missing or invalid parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "url is required"
