# ScreenshotAPI

> Self-hosted Puppeteer-based screenshot API. Capture any webpage as PNG/JPEG/WebP/PDF, batch multiple URLs in parallel, compare pages with pixel-level visual diffs, extract Markdown content alongside images, and run async jobs with webhook callbacks. Built on Fastify + TypeScript with Redis caching.

## Base URL

http://localhost:3000

## Authentication

Optional. When `API_KEYS` env var is set, pass your key via:
- Header: `X-Api-Key: your-key`
- Query param: `?api_key=your-key`

## Endpoints

### GET /screenshot

Capture a single screenshot.

Required (one of):
- `url` — page URL (http/https only; private IPs are blocked)
- `html` — base64-encoded HTML to render

Viewport:
- `width` (default 1280, max 3840), `height` (default 800, max 2160)
- `device_scale_factor` (default 1, e.g. 2 for retina)
- `viewport_mobile=true` — mobile emulation

Capture:
- `format` — `png` | `jpeg` | `webp` | `pdf` (default `png`)
- `quality` — 1–100 for jpeg/webp (default 80)
- `full_page=true` — full scrollable height
- `omit_background=true` — transparent background (PNG)
- `selector` — capture only this CSS element
- `clip_x`, `clip_y`, `clip_width`, `clip_height` — crop region
- `image_width`, `image_height` — resize output

Timing:
- `delay` — extra wait in ms after load (max 10000)
- `wait_until` — `load` | `domcontentloaded` | `networkidle0` | `networkidle2` (default)
- `wait_for_selector` — wait for CSS selector to appear

Emulation:
- `dark_mode=true` — prefers-color-scheme: dark
- `reduced_motion=true`
- `media_type` — `screen` | `print`
- `timezone` — IANA timezone, e.g. `America/New_York`
- `geolocation_latitude`, `geolocation_longitude`, `geolocation_accuracy`
- `user_agent` — custom User-Agent

Injection:
- `headers` — JSON object of extra HTTP headers
- `cookies` — JSON array of cookies
- `styles` — CSS to inject before capture
- `scripts` — JS to inject before capture
- `hide_selectors` — comma-separated CSS selectors to hide
- `click` — CSS selector to click before capture

Blocking:
- `block_ads=true`, `block_trackers=true` — Ghostery-powered
- `block_cookie_banners=true`
- `block_requests` — comma-separated URL patterns

PDF:
- `pdf_print_background=true`
- `pdf_landscape=true`
- `pdf_paper_format` — `a4` | `a3` | `a2` | `a1` | `letter` | `legal` | `tabloid`

Content extraction (adds `content` field to JSON response):
- `extract_content=true`
- `content_format` — `markdown` (default) | `html` | `text`

Metadata (adds `metadata` field):
- `metadata=true` — returns title, description, og_title, og_image, og_description, favicon, http_status

Async:
- `async=true` — returns `{ job_id, status }` immediately (HTTP 202)
- `webhook_url` — POST result to this URL when done

Cache & response:
- `cache=false` — bypass cache, force fresh capture
- `response_type` — `image` (binary, default) | `json` (metadata only) | `base64` (image in JSON)

Response headers: `X-Cache: HIT|MISS`, `Cache-Control`, `ETag`

### POST /batch

Capture up to 10 URLs in parallel.

Body (JSON):
```json
{
  "urls": ["https://example.com", "https://google.com"],
  "format": "png",
  "width": 1280,
  "height": 800,
  "quality": 80,
  "full_page": false,
  "response_type": "base64"
}
```

`urls` is required (1–10 items). `response_type`: `base64` | `paths`.

Response:
```json
{
  "total": 2,
  "results": [
    { "url": "https://example.com/", "format": "png", "cached": false, "image": "data:image/png;base64,..." },
    { "url": "https://google.com/", "error": "Navigation timeout" }
  ]
}
```

Failed URLs return `{ url, error }` without stopping the rest. SSRF-blocked URLs fail the entire request with 403.

### GET /diff

Pixel-level diff between two URLs. Returns highlighted diff image and change stats.

Parameters:
- `before` — "before" URL (required)
- `after` — "after" URL (required)
- `width` (default 1280), `height` (default 800)
- `threshold` — 0–1, sensitivity (default 0.1; lower = more sensitive)
- `response_type` — `image` (PNG binary) | `json` (stats only) | `base64` (image + stats)

JSON response:
```json
{
  "before": "https://...",
  "after": "https://...",
  "width": 1280,
  "height": 800,
  "changed_pixels": 1420,
  "total_pixels": 1024000,
  "diff_percent": 0.14,
  "has_changes": true
}
```

### GET /jobs/:id

Check status of an async job.

Response:
```json
{
  "id": "550e8400-...",
  "status": "done",
  "createdAt": "2024-01-15T12:00:00.000Z",
  "completedAt": "2024-01-15T12:00:03.000Z",
  "result": { "url": "https://...", "format": "png", "storagePath": "storage/..." }
}
```

Statuses: `pending` → `processing` → `done` | `failed`. Jobs expire after 1 hour.

### GET /health

```json
{ "status": "ok", "browserPool": { "size": 3, "available": 2 }, "cache": "redis" }
```

No auth required.

## Error Format

All errors return JSON:
```json
{ "error": "ERROR_CODE", "message": "Human-readable explanation" }
```

Codes: `VALIDATION_ERROR` (400), `INVALID_URL` (400), `UNAUTHORIZED` (401), `BLOCKED` (403 — SSRF), `TIMEOUT` (408), `NAVIGATION_FAILED` (422), `SELECTOR_NOT_FOUND` (422), `RATE_LIMITED` (429), `POOL_EXHAUSTED` (503).

## MCP Server

This API ships an MCP server for direct LLM integration (Claude Desktop, Cursor, etc.).

Build: `npm run build` in the backend directory.

Add to `~/.claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "screenshot": {
      "command": "node",
      "args": ["/absolute/path/to/screenshot-backend/dist/mcp.js"],
      "env": {
        "SCREENSHOT_API_URL": "http://localhost:3000",
        "SCREENSHOT_API_KEY": "your-key"
      }
    }
  }
}
```

Available MCP tools:
- `screenshot(url, format, width, height, full_page, dark_mode, wait_for_selector, delay, block_ads, block_cookie_banners, extract_content, metadata)` — returns image + metadata
- `batch_screenshot(urls, format, width, height)` — returns one image per URL
- `visual_diff(before, after, threshold, width, height)` — returns diff image + stats JSON
- `check_job(job_id)` — returns job status JSON

## Quick Start

```bash
# Docker (recommended — includes Redis)
API_KEYS=mykey docker compose up

# Local
cp .env.example .env && npm install && npm run dev

# Take a screenshot
curl "http://localhost:3000/screenshot?url=https://example.com" \
  -H "X-Api-Key: mykey" --output out.png
```
