---
title: "Render a URL or HTML"
method: POST
path: "/api/render"
tags: ["Rendering"]
---

# Render a URL or HTML

`POST /api/render`

## Request

- Base URL: `https://snaplab.dev`
- URL: `https://snaplab.dev/api/render`
- Auth: HTTP bearer

The main endpoint. Give it a `url` **or** inline `html`/`template` and it answers with the bytes — or, with `responseType: "json"`, a hosted URL.

## Request body

- RenderOptions: Exactly one of `url` or `html`/`template` must be given.
  - `url` string, uri: Absolute http(s) URL to capture. Private, loopback, link-local and metadata addresses are refused.
  - `html` string: Raw HTML rendered instead of navigating. Subresources are still SSRF-filtered.
  - `template` string: HTML with `{{ path.to.value }}` placeholders, substituted from `data` and HTML-escaped.
  - `data` object: Values for `template` (or for `html`, when no template is given).
  - `css` string: Stylesheet injected into `<head>` before load. html/template mode only.
  - `googleFonts` string: Comma-separated Google Fonts families, e.g. `Inter,Playfair Display` (max 6).
  - `format` 'png' | 'jpeg' | 'webp' | 'avif' | 'pdf'
  - `viewport` 'desktop' | 'laptop' | 'tablet' | 'mobile' | 'wide': Preset size. `width`/`height`/`deviceScaleFactor` override individual axes.
  - `width` integer
  - `height` integer
  - `deviceScaleFactor` number: Pixel ratio. 2 = retina.
  - `mobile` boolean: Sugar for `viewport: "mobile"` (390x844 @3x, touch emulation).
  - `fullPage` boolean: Capture the whole scrollable document. Ignored when `selector` is set.
  - `selector` string: Capture just this element. PDFs stay vector/selectable.
  - `quality` integer: jpeg/webp/avif only.
  - `waitUntil` 'auto' | 'load' | 'domcontentloaded' | 'networkidle': "auto" waits for load, then for the network to be quiet for 150ms, capped at 400ms. "networkidle" is the full Playwright wait and costs ~0.5s more.
  - `waitForSelector` string: Wait (up to 10s) for this selector before capturing.
  - `delayMs` integer
  - `scrollY` integer: Scroll down this many pixels first (triggers lazy loading).
  - `hideSelectors` string[]: Elements to hide before capturing. Comma-separated in a query string.
  - `darkMode` union: `true` (= `"force"`) emulates `prefers-color-scheme: dark` AND turns on Chromium's auto dark mode, so a page with no dark theme comes back dark; a page that has one is left alone. `"prefers"` is the media query only.
    - boolean
    - 'prefers' | 'force'
  - `dismissBanners` boolean: Hide known cookie/consent/chat banners with a curated stylesheet.
  - `blockAds` boolean: Abort requests to known ad, analytics and session-replay hosts.
  - `customCss` string: CSS injected after load.
  - `customJs` string: JavaScript evaluated after load.
  - `cookies` object[]: Per-request cookies. Renders with cookies are never cached or shared.
    - `name` string, required
    - `value` string, required
    - `domain` string: Defaults to the target host.
    - `path` string
    - `secure` boolean
    - `httpOnly` boolean
    - `sameSite` 'Strict' | 'Lax' | 'None'
    - `expires` integer: Unix seconds. Omit for a session cookie.
  - `headers` object: Extra request headers.
  - `basicAuth` object
    - `username` string, required
    - `password` string, required
  - `pdf` object: PDF-only geometry. Ignored unless `format` is `pdf`.
    - `format` 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6'
    - `landscape` boolean
    - `printBackground` boolean
    - `scale` number
    - `preferCSSPageSize` boolean: Honour the page's own `@page size` rule instead of `format`.
    - `margin` object: CSS lengths (`0.4in`, `1cm`, `12mm`…).
      - `top` string
      - `right` string
      - `bottom` string
      - `left` string
    - `headerTemplate` string: HTML drawn at the top of every page, inside the top margin. It inherits none of the document's CSS, so set `font-size` explicitly and add your own side padding. Any element carrying the classes `pageNumber`, `totalPages`, `date`, `title` or `url` is filled in by Chromium.
    - `footerTemplate` string: Same as `headerTemplate`, at the bottom of every page. Also accepts a built-in preset name: `@pageNumbers` (centred “Page N of M”) or `@pageNumbersUrl` (page numbers left, document URL right).
    - `displayHeaderFooter` boolean: Draw the running header and footer. Defaults to `true` as soon as either template is set; pass `false` to suppress both.
    - `pageRanges` string: Sheets to keep, in print-dialog syntax — `1-3`, `1,4,7-9`. Default: every page.
    - `media` 'screen' | 'print': Which stylesheet the paper is printed under. `print` honours the document's own print CSS (`@media print` rules, page breaks, hidden nav); `screen` keeps the on-screen look. Screenshots are always screen media.
    - `tagged` boolean: Emit a tagged (accessible) PDF with structure tags for screen readers.
    - `outline` boolean: Embed a document outline (PDF bookmarks) built from the page's headings.
    - `width` string: Exact paper width as a CSS length. Replaces `format`; ignored unless `height` is set too.
    - `height` string: Exact paper height as a CSS length.
    - `omitBackground` boolean: Drop background colours and images. The same knob as `printBackground: false`, and it wins over it.
  - `filename` string: Suggested download name, sanitised. Sets `content-disposition: inline; filename="…"` on the returned bytes and on the hosted `/i/c/{id}` file (`?download=1` there makes it an `attachment`). Include the extension you want — it is used verbatim.
  - `cache` boolean: Set false to force a fresh render.
  - `responseType` 'binary' | 'json': `binary` streams the bytes; `json` stores the render and answers with a hosted URL.

## Response `200`

The rendered bytes.

- HostedRender: Answer for `responseType: "json"` — the render is stored and served from a public, immutable URL.
  - `url` string, uri, required
  - `id` string, required
  - `format` string, required
  - `width` integer
  - `height` integer
  - `bytes` integer, required
  - `cacheHit` boolean
  - `durationMs` integer

## Other responses

- `400`: Invalid options, or a target that resolves to a private/blocked address.
- `401`: Missing or invalid API key / signature.
- `402`: Subscription past due (`payment_required`).
- `422`: The render can never succeed as written (`selector_not_found`).
- `429`: Rate limit or monthly quota exceeded (`quota_exceeded`).
- `502`: The target site was unreachable (`upstream`).
- `503`: Queue saturated or browsers not ready. Retry after `Retry-After` seconds.
- `504`: The page did not finish in time (`timeout`).

---

[API](https://skmtc.dev/snaplab/apis/snaplab-api.md) · [All operations](https://skmtc.dev/snaplab/apis/snaplab-api/llms.txt) · [OpenAPI document](https://skmtc.dev/snaplab/apis/snaplab-api/revisions/4dbfbd8cd118?raw)
