Openkova is a self-hosted screenshot API that converts HTML to image and PDF. Paste a snippet, upload .html files, or point it at a live URL — Openkova renders each page in headless Chromium and returns a PNG, JPEG, WebP, or PDF.
MIT license — free for commercial use, no licensing restrictions. (The main alternative, Browserless, uses SSPL-1.0, which requires a commercial license for closed-source use.)
Full documentation and hosted version at openkova.dev · API docs · Docs · Blog
| Package | Description |
|---|---|
@openkova/core |
Node.js library — headless Chromium renderer, crawler, and storage adapter. Powers the web app and CLI. |
@openkova/cli |
CLI for screenshots from the terminal or CI/CD pipelines (npx @openkova/cli). |
@openkova/mcp |
Local MCP server for AI clients — Claude Desktop, Cursor, Windsurf (npx @openkova/mcp). |
npm install @openkova/core
# or
pnpm add @openkova/core
# or
bun add @openkova/coreimport { screenshotSnippet, createSession } from '@openkova/core';
const sessionId = createSession();
const imageId = await screenshotSnippet('<h1>Hello</h1>', sessionId, {
viewport: { width: 1280, height: 800 },
fullPage: true,
format: 'png', // 'png' | 'jpeg' | 'webp' | 'pdf'
});See packages/core/README.md for the full API reference.
npx @openkova/cli screenshot https://example.com
npx @openkova/cli snippet --html '<h1>Hello</h1>' --format pdf
npx @openkova/cli crawl https://example.com --depth 2 --out ./screenshots/See packages/cli/README.md for all commands and flags.
Connect any MCP-compatible AI client (Claude Desktop, Cursor, Windsurf) to take screenshots locally:
{
"mcpServers": {
"kova": {
"command": "npx",
"args": ["@openkova/mcp"]
}
}
}See packages/mcp/README.md for setup and tool reference.
- HTML Snippet — paste raw HTML and screenshot it at any viewport size
- File upload — upload one or more
.htmlfiles, each rendered separately - URL crawl — screenshot a live site and same-origin linked pages, 10 at a time (up to 200 pages per crawl)
- Output formats — PNG, JPEG, WebP, or PDF
- Full-page capture — capture the full scrollable height, not just the viewport
- Viewport selection — Mobile (390px), Desktop (1280px), or Wide (1920px)
- Live terminal — real-time SSE progress stream as the browser captures pages
- REST API — all conversions available as SSE-streaming HTTP endpoints
- Download All — ZIP download of all files in a session
- Next.js (App Router, standalone output)
- Puppeteer Core + system Chromium
- pnpm workspaces monorepo —
packages/core,packages/cli,packages/mcp,apps/web - Deployed on Railway via Docker
- Node.js 18+
- pnpm 10+
- Google Chrome or Chromium installed locally
npm install -g pnpmpnpm install
pnpm devThe app starts at http://localhost:3000.
By default the dev server uses the Chromium bundled with the puppeteer dev dependency. To use a specific binary set CHROMIUM_PATH:
CHROMIUM_PATH=/usr/bin/chromium pnpm devScreenshots are saved to ./data by default. Override with OPENKOVA_STORAGE_PATH.
docker compose up --buildThis builds the multi-stage image (Node + system Chromium on Debian) and mounts a persistent volume at /data for screenshots.
All convert endpoints stream Server-Sent Events rather than a single JSON response. Progress messages arrive as progress events; the final result arrives as a done event.
POST /api/convert/snippet { html, sessionId?, viewport?, fullPage?, format? }
POST /api/convert/file multipart/form-data files[], sessionId?, viewport?, fullPage?, format?
POST /api/convert/url { url, depth?, sessionId?, viewport?, fullPage?, format? } — crawl mode
POST /api/convert/url { urls[], sessionId, offset, total, viewport?, fullPage?, format? } — paginate mode
GET /api/image/:sid/:id → file binary (PNG/JPEG/WebP/PDF)
GET /api/session/:sid/download → ZIP of all session files
GET /api/session/:sid → { images: string[] }
format accepts "png" (default), "jpeg", "webp", or "pdf". The returned imageId includes the file extension (e.g. abc123.jpg).
Full API documentation at openkova.dev/screenshot-api.
const res = await fetch('/api/convert/snippet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html: '<h1>Hello</h1>',
viewport: { width: 1280, height: 800 },
fullPage: true,
format: 'pdf',
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
for (const part of buffer.split('\n\n').slice(0, -1)) {
const line = part.split('\n').find(l => l.startsWith('data: '));
if (!line) continue;
const event = JSON.parse(line.slice(6));
if (event.type === 'done') console.log(event.data); // { sessionId, imageId, url }
}
buffer = buffer.split('\n\n').pop() ?? '';
}apps/
web/ Next.js app (UI + API routes)
packages/
core/ Headless renderer, crawler, storage adapter
cli/ Command-line interface (npx @openkova/cli)
mcp/ Local MCP server for AI clients (npx @openkova/mcp)
Dockerfile Multi-stage build for Railway/Docker
railway.toml Railway deployment config
Files are automatically deleted 24 hours after the session directory was last written to. A cleanup pass runs every hour on server startup via instrumentation.ts. Download your files before then using the Download All button or the /api/session/:sid/download ZIP endpoint.
| Variable | Default | Description |
|---|---|---|
CHROMIUM_PATH |
auto-detected | Path to Chrome/Chromium binary |
OPENKOVA_STORAGE_PATH |
./data |
Directory for saved files |
PORT |
3000 |
HTTP port |
MIT — free for commercial use, no licensing restrictions.