---
title: "Render and store a capture"
method: POST
path: "/api/captures"
tags: ["Captures"]
---

# Render and store a capture

`POST /api/captures`

## Request

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

Renders synchronously and keeps the result, so it stays available at `/i/c/{id}` and in the dashboard. Pass `webhookUrl` to be called back when the capture finishes instead of polling — see `WebhookPayload`.

## Request body

- CaptureRequest: 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 completed capture row.

- Capture
  - `id` string
  - `userId` string
  - `apiKeyId` string, nullable
  - `url` string
  - `title` string, nullable
  - `status` 'pending' | 'processing' | 'completed' | 'failed'
  - `format` string
  - `selector` string, nullable
  - `fullPage` boolean
  - `width` integer
  - `height` integer
  - `deviceScaleFactor` integer
  - `darkMode` boolean
  - `fileKey` string, nullable
  - `fileSize` integer, nullable
  - `fingerprint` string, nullable
  - `error` string, nullable
  - `durationMs` integer, nullable
  - `cacheHit` boolean
  - `createdAt` string, date-time
  - `completedAt` string, date-time, nullable
  - `webhook` CaptureWebhook: Present on `GET /api/captures/{id}` only when a callback was set.
    - `url` string, uri
    - `deliveries` WebhookDelivery[]
      - `id` string
      - `attempt` integer: Attempts made so far, 1-4.
      - `status` 'pending' | 'ok' | 'failed' | 'gave_up'
      - `lastStatusCode` integer, nullable: HTTP status of the last attempt; null if the request never completed.
      - `updatedAt` string, date-time

## Other responses

- `400`: Invalid options, a target that resolves to a private/blocked address, or a `webhookUrl` that is not https or resolves to a private 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)
