# 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