# snaplab > Rendering API for developers. Turns any URL or HTML string into a screenshot > (PNG, JPEG, WebP, AVIF), a PDF, or a social/Open Graph image, using headless > Chromium. Plain HTTP, no SDK required. > Base URL: https://snaplab.dev ## Summary snaplab is a hosted rendering API. You send a URL or a block of HTML and get back rendered bytes. It exists so developers do not have to run, patch, scale and babysit their own headless Chromium fleet for what is usually a small part of their product: screenshots for previews and monitoring, PDFs for invoices and reports, and 1200x630 images for social cards. The main server-side use case is PDFs of your own authenticated pages: pass a per-request `cookies` array, `headers` (e.g. a one-time render token) or `basicAuth` and snaplab prints the invoice, statement, report or chat-transcript route your app already renders behind its login — credentials are scoped to the target host, never persisted, and credentialed renders are never cached or shared. Pricing: Free 100 renders/month (no card), Starter $9/mo for 2,000, Pro $29/mo for 10,000, Business $79/mo for 40,000. Annual billing is ten months' price for twelve — Starter $90/yr, Pro $290/yr, Business $790/yr — with the same monthly render allowance. Cache hits and failed renders do not count against quota. Exceeding the quota is a hard stop with a 429, never an overage charge. Billing via Stripe, cancel any time. API keys look like `sk_` followed by a random string, created at /app/keys. ## Authentication `Authorization: Bearer sk_...` on every request. Signed URLs, for `` and `og:image` where a key must not appear, work on `GET /api/render` and `GET /api/render/image`: add `keyId` (the API key's id) and `sig`, where `sig = hex(HMAC_SHA256(signingSecret, canonical))`. The canonical string is every query pair except `sig`, sorted by name, percent-encoded with a standard encoder, joined with `&`. Sign the exact string you emit, then append `&sig=...` to it. An optional `expires` (unix seconds) limits the lifetime. The account signing secret is shown once in the dashboard at /app/keys. ## Endpoints Errors are JSON: `{"error": "...", "code": "...", "issues": [...]}`. - `POST /api/render` — URL or HTML to image or PDF. JSON body. Options: url | html (one required), format=png|jpeg|webp|avif|pdf (png), fullPage (true), viewport=desktop|laptop|tablet|mobile|wide (desktop, 1440x900 @2x), width, height, deviceScaleFactor, selector, quality, waitUntil=auto|load|domcontentloaded|networkidle (auto: load, then a 150ms network lull, capped at 400ms; networkidle is the full wait, ~0.5s slower), waitForSelector, delayMs, scrollY, hideSelectors[], darkMode=true|false|"prefers"|"force" (true/"force": prefers-color-scheme dark PLUS Chromium auto-dark, so pages with no dark theme come back dark; "prefers": the media query only), dismissBanners (true), blockAds (false), mobile, customCss, customJs, cookies[], headers{}, basicAuth{username,password}, pdf{format,landscape,printBackground,margin{top,right,bottom,left},scale, preferCSSPageSize,headerTemplate,footerTemplate,displayHeaderFooter, pageRanges,media=print|screen (print: the page's own print CSS applies), tagged,outline,width,height,omitBackground}, filename (download name, sets content-disposition; footerTemplate also takes the presets "@pageNumbers" and "@pageNumbersUrl", and header/footer templates fill in elements with class pageNumber|totalPages|date|title|url), cache (true), responseType=binary|json (binary). - `GET /api/render?url=...` — same option names as query parameters. Booleans are "true"/"false", hideSelectors is comma-separated, PDF page size is `pdfFormat`. Accepts a Bearer key OR a signed URL. Example: `/api/render?url=https%3A%2F%2Fexample.com&fullPage=true&hideSelectors=.a,.b&pdfFormat=A4` - `POST|GET /api/render/image` — HTML/CSS to image, the OG card path. Options: html | template + data ({{var}} substitution, values HTML-escaped), css, width (1200), height (630), deviceScaleFactor (2), format (png), googleFonts ("Inter,Playfair Display"), darkMode, responseType. - `POST /api/captures` — same options, queued and persisted; returns a capture record. `GET /api/captures` lists them. Captures persist until deleted. Optional `webhookUrl` (https only) + `webhookSecret` POST `{event: "capture.completed"|"capture.failed", capture, hostedUrl}` on finish, signed `x-snaplab-signature: sha256=hex(HMAC_SHA256(secret, x-snaplab-timestamp + "." + rawBody))`; 2xx only, retried +30s/+5min/+30min. Account defaults: `PATCH /api/me {webhookUrl, webhookSecret}` (GET masks the secret). - `POST /api/tools/screenshot | /api/tools/pdf | /api/tools/image` — no API key, 20 renders/day/IP, always returns JSON with a hosted url. - `GET /api/health` — no auth. With `responseType: "json"` a render returns `{url, id, format, width, height, bytes, cacheHit, durationMs}` where url looks like `https://snaplab.dev/i/c/`. Response headers: `x-snaplab-cache` (HIT|MISS), `x-snaplab-fingerprint`, `x-snaplab-duration-ms`, `x-snaplab-quota-remaining`. Errors: 400 validation or a blocked/private target, 401 invalid key or bad signature, 402 payment_required, 429 quota_exceeded or rate limited (carries retry-after), 503 overloaded or renderer not ready. Only 429 rate limits and 503 are worth retrying. ## Limits and behaviour - Caching: keyed by a fingerprint of the normalised options, TTL 24 hours. Cache hits return `x-snaplab-cache: HIT` and do not count against quota. `"cache": false` forces a fresh render and stores nothing. - Rate limits per plan: 30/min and 1 concurrent render (Free), 120/min and 3 (Starter), 300/min and 5 (Pro), 600/min and 10 (Business). - URL restrictions: http and https only; private, loopback, link-local and CGNAT addresses are blocked, including on redirects. localhost and private staging hosts cannot be rendered — use a tunnel, or send the markup as `html`. - Viewport capped at 3840x4320. `html` up to 2 MB. customCss/customJs 20,000 characters. Up to 20 hideSelectors and 40 cookies. ## Integrations - JavaScript/TypeScript SDK: `npm install snaplab`. Zero dependencies, ESM + CJS, typed options. `new Snaplab({ apiKey })` then `screenshot()`, `pdf()`, `image()` (Uint8Array, or a hosted record with `responseType: "json"`), `capture()`, `waitForCapture()`, `usage()`, and the static `Snaplab.signUrl()` for signed URLs. Errors throw `SnaplabError` with status, code and body. - Python SDK: `pip install snaplab-sdk`, imports as `snaplab_sdk` (plain `snaplab` on PyPI is an unrelated project). Zero dependencies, Python 3.9+, typed. `Snaplab(api_key)` then `screenshot()`, `pdf()`, `image()` (bytes, or a dict with `response_type="json"`), `capture()`, `wait_for_capture()`, `usage()`, plus `sign_url()` and `verify_webhook_signature()`. Options are snake_case (`full_page`, `device_scale_factor`, `block_ads`) and converted to camelCase; errors raise `SnaplabError` with status, code and body. - MCP (Model Context Protocol), Streamable HTTP at https://snaplab.dev/mcp, auth `Authorization: Bearer sk_...`. Tools: screenshot, pdf, html_to_image, usage. Claude Code: `claude mcp add --transport http snaplab https://snaplab.dev/mcp --header "Authorization: Bearer sk_..."` - n8n: community node `n8n-nodes-snaplab` (self-hosted), or a plain HTTP Request node. - Apify actor: "snaplab Screenshot & PDF Renderer". - Zapier and Make: generic HTTP module, POST JSON with `responseType: "json"`. ## Free tools (no account, 20 renders/day per IP) - https://snaplab.dev/tools/website-screenshot - https://snaplab.dev/tools/html-to-pdf - https://snaplab.dev/tools/og-image-generator ## Pages - Home: https://snaplab.dev/ - API reference: https://snaplab.dev/docs - Integrations: https://snaplab.dev/docs#integrations - Pricing: https://snaplab.dev/#pricing - OpenAPI: https://snaplab.dev/openapi.json - Blog: https://snaplab.dev/blog - Terms: https://snaplab.dev/terms - Privacy: https://snaplab.dev/privacy ## Contact snaplab.dev