Skip to content

Screenshot API

Capture any URL — or a raw HTML string — in the same real headless Chrome that renders templates. What you get back is what a browser shows.

What it captures#

Captures run in a real browser rather than an HTML-to-image approximation, so the things that usually break in screenshot services simply work:

  • Real web fonts, including self-hosted and variable fonts
  • CSS grid, flexbox, container queries and custom properties
  • Canvas, SVG, and web components inside a shadow DOM
  • Lazy-loaded images and content that appears only after scroll
  • Cookie banners and overlays you can dismiss before the shot
GET /api/v1/screenshot/options returns the full option set this deployment accepts, live and without an API key — treat it as the authoritative list if this page and the endpoint ever disagree.

Response modes#

By default a capture is stored and the response carries a CDN URL. Identical requests inside the cache window are served from cache and are not charged — the cached flag on the response tells you which happened.

Saved presets#

A preset is a named set of capture options stored in your workspace. Save the whole recipe once — viewport, format, scale, selectors to hide, cookies to set — then capture by naming it, instead of repeating sixty fields on every call.

The point is division of labour. The person who knows that a site needs #intercom-container hidden and its cookie banner dismissed is rarely the person who owns the cron job. Edit the preset in the dashboard and every job that names it picks the change up on its next run — no redeploy, and the options live in one place rather than being copied into each caller.

1. Save the options

Save from the dashboard under Screenshots, or over the API. Names are unique per workspace, so saving under an existing name edits it rather than creating a duplicate. Options are validated as they are stored — a preset that could never capture is rejected here, not later on reuse.

Save
curl -X POST https://pixbix.app/api/v1/screenshot/presets \
  -H "x-api-key: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pricing page — retina",
    "options": {
      "url": "https://acme.in/pricing",
      "format": "png",
      "fullPage": true,
      "scale": 2,
      "viewport": { "width": 1440, "height": 900 },
      "blockCookieBanners": true,
      "hideSelectors": ["#intercom-container"],
      "waitUntil": "networkidle2"
    }
  }'

2. Capture with it

Send presetId and nothing else. Every option the preset holds is applied, including the URL — so this is a complete capture request.

curl -X POST https://pixbix.app/api/v1/screenshot \
  -H "x-api-key: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "presetId": "prs_9d2f" }'

3. Override what changes

Anything you send alongside presetId wins. The usual case is reusing one recipe across many pages by overriding only the URL.

Override
{
  "presetId": "prs_9d2f",
  "url": "https://acme.in/enterprise",
  "viewport": { "width": 390 }
}
Merging is deep, so viewport.widthabove overrides the width and keeps the preset’s height: 900. Arrays replace rather than append — sending "hideSelectors": []is how you clear a preset’s list for one capture.

Across a batch

A top-level presetId applies to every item, which is the shortest way to capture a list of URLs under one recipe. An item may name its own preset, which wins for that item.

Batch
{
  "presetId": "prs_9d2f",
  "items": [
    { "url": "https://acme.in/pricing" },
    { "url": "https://acme.in/enterprise" },
    { "url": "https://acme.in/startups", "fullPage": false }
  ]
}
  • Presets belong to the workspace, not to whoever created them — a recipe survives someone leaving.
  • Delivery fields (response, download, filename) are never stored; they are decided per capture.
  • Managing presets costs no credits. A capture that uses one costs exactly what the same capture costs without one.
  • An unknown presetId fails with 404 PRESET_NOT_FOUND rather than silently capturing with the options you did send.

Reading values off the page#

A capture is a photograph of a page, and a photograph is rarely all you wanted from it. The price, the headline and the stock count are on that page. extract reads them and returns them beside the image.

The values are read in the same navigation as the capture — after the same consent clicks, the same cookies, the same login, at the same moment. Fetching the page separately can legitimately return different values from the ones visible in the picture you were given.
curl -X POST https://pixbix.app/api/v1/screenshot \
  -H "x-api-key: $PIXBIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.in/product/kettle",
    "extract": {
      "title": "h1",
      "price": ".price"
    }
  }'
OptionMeaning
keyWhat the value is called in `extracted`. Letters, numbers and underscores.
selectorCSS selector of the element to read.
attributeRead an attribute instead of the text — href, src, content.
allReturn every match as an array rather than the first as a string.
indexRead one specific match, counting from 0, when the value you want is the third .price on the page rather than the first. Out of range reads as null. Cannot be combined with all, which already returns every match.
layerIdImage templates: the layer this value fills — text, a picture, or a data element.
layerFieldWhich slot of that layer — values, labels, title, column:0, value, label, suffix, max. Unset takes the primary one. Append a position to fill one entry: values:2, column:1:4.

A selector that matches nothing comes back null — deliberately distinct from "", which means the element was found and is empty. A bad selector never fails the capture; you get the picture and a null.

In workflows

A capture step’s extracted values land on the row itself, so a later step reads them as {{item.price}} — the same expression it would use for a spreadsheet column. That is what makes a capture a data source: shoot a product page, then render a card from what was on it. A column the row already has always wins, so a mapping you made by hand is never silently replaced.

In image templates

A live-capture layer can carry extractions too, and each one may name a layerId. The captured page then fills that layer on every render: shoot the product page, and the headline beside the screenshot is that product’s name. A value passed in by the caller wins over one read from the page, since an explicit field is a deliberate instruction.

What the layer does with the value depends on what it is. A text layer takes it as words. An image layer takes it as a picture — read the src of the hero photo or the content of og:image, and that layer becomes the product’s own photo at full size, composited into the design rather than cropped out of a screenshot of the page it sat on. A relative address resolves against the page it was read from; anything that is not an http(s) address after that is left alone, and the layer keeps its existing picture.

A data element — a chart, a table, a widget, a QR code — is fed the same way, with layerField naming which part of it to fill. A chart takes values, labels or title, and series:2 when it plots several series and you mean one of them; a table column:0, column:1 and so on; a widget value, label, suffix or max; a code layer value. Omit it and each takes its primary slot.

Because the slot is named, several extractions can feed one element: read the product names with allinto a table’s first column and the prices into its second, from one visit. Quantities are parsed out of whatever the page prints — ₹1,299, 73% and 1.234,50 all arrive as numbers — and anything with no number in it is left out rather than plotted as zero.

A list-shaped slot can also be addressed one entry at a time by appending its position: values:2 is the third bar, labels:0 the first label, and column:1:4 one cell — column 1, row 4. Without an index the whole list is replaced, which is what an all extraction is for; with one, a single figure updates and the rest of the chart stays as it was drawn. An index past the end grows the series to reach it rather than writing nowhere — a bar beyond the last label is drawn under its position number, and a column beyond the last heading is added to the table with a blank one, so a figure that was read is never a figure that goes missing.

Every extraction also comes back on the render itself, under extracted — including the ones that name no layerId and so appear nowhere in the picture. It is on the JSON response, on the render when you fetch it back, and in the render.completed webhook; a binary render carries it in the X-Pixbix-Extracted header instead, since bytes have nowhere else to put it.

Endpoints#

Each endpoint runs live from this page. The options and status endpoints need no key at all, so they work immediately.

Capture a screenshot

POST/api/v1/screenshotNo auth
Renders any URL — or a raw HTML string — in real headless Chrome and returns an image or PDF. Real web fonts, CSS grid, canvas and lazy-loaded images all behave as they do in a browser.

Cost 1 image credit per capture.

Body parameters

url
stringoptional
Page to capture. Provide either url or html.
html
stringoptional
Raw HTML to render instead of fetching a URL.
presetId
stringoptional
Apply a saved preset as the baseline. Anything else you send overrides it, field by field — nested objects merge, so setting viewport.width keeps the preset's height.
format
"png" | "jpeg" | "webp" | "avif" | "pdf"optional
Defaults to png.
fullPage
booleanoptional
Capture the entire scroll height.
viewport.width
numberoptional
Defaults to 1280.
viewport.height
numberoptional
Defaults to 800.
selector
stringoptional
Capture only the first element matching this CSS selector.
waitUntil
stringoptional
load, domcontentloaded, networkidle0 or networkidle2.
cookies
arrayoptional
Cookies to set before navigating, for pages behind a login: [{ name, value, domain?, path?, httpOnly?, secure?, expires? }]. domain defaults to the host of url.
authenticate
objectoptional
HTTP basic auth: { username, password }.
response
"url" | "async" | "binary" | "base64" | "json"optional
How you want the capture back. Defaults to url — the image is stored and you get a link. binary returns the raw bytes, base64 returns them inline, json returns the metadata with no image. async does not wait: you get 202 with a job id straight away, and poll GET /v1/renders/:id until it reports done. Use it whenever you are not showing the picture to somebody who is waiting for it — a capture is at the mercy of the page it is capturing, and a slow site should cost you a poll rather than a held-open request and a client timeout.
extract
array | objectoptional
Values to read off the page alongside the picture, returned under `extracted`. Either an array of { key, selector, attribute?, all?, index?, layerId?, layerField? } or the shorthand { key: selector }. `attribute` reads an attribute instead of the visible text (href, src, content); `all` returns every match as an array; `index` reads one specific match, counting from 0, for the page where the value you want is the third `.price` on it. `all` and `index` cannot be combined. In an image template, `layerId` points the value at a layer — text takes it as words, an image layer as its picture, and a chart, table, widget or code layer as data, with `layerField` naming the slot (`values`, `labels`, `title`, `column:0`, `value`, `label`, `suffix`, `max`) and an optional position filling one entry of it (`values:2`, `column:1:4`). A chart of several series addresses one of them as `series:2`, or one entry of it as `series:2:0`. Several extractions may feed one element — each writes only the slot it names, and they compose, so three reads fill three bars of the same chart. A position past the end grows the element to reach it. Read in the same navigation as the capture, so the values always describe the image beside them. Up to 25 per request.
{
  "url": "https://example.com",
  "format": "png",
  "fullPage": true,
  "viewport": {
    "width": 1440,
    "height": 900
  },
  "extract": {
    "headline": "h1",
    "price": ".price",
    "canonical": "link[rel=canonical]"
  }
}

Try it

/api/v1/screenshot
Show as cURL
curl -X POST "https://pixbix.app/api/v1/screenshot" \
  -H "x-api-key: pk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com", "format": "png", "fullPage": true, "viewport": { "width": 1440, "height": 900 }, "extract": { "headline": "h1", "price": ".price", "canonical": "link[rel=canonical]" } }'

Capture via query string

GET/api/v1/screenshotNo auth
The same capture expressed as a GET, so it can be dropped straight into an <img> tag or a spreadsheet formula.

Cost 1 image credit per capture, unless served from cache.

Query parameters

presetId
stringoptional
Apply a saved preset as the baseline. Any other parameter you send overrides it.
url
stringrequired
Page to capture.
format
stringoptional
Defaults to png.
fullPage
booleanoptional
Capture the entire scroll height.
Response
{
  "success": true,
  "data": {
    "url": "https://cdn.pixbix.app/render/org_4c1d/cap_6f0e2a/homepage.png",
    "cached": true
  }
}

Try it

/api/v1/screenshot?presetId=prs_9d2f&url=https%3A%2F%2Fexample.com&format=png&fullPage=true
Show as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot?presetId=prs_9d2f&url=https%3A%2F%2Fexample.com&format=png&fullPage=true" \
  -H "x-api-key: pk_live_your_key"

Batch capture

POST/api/v1/screenshot/batchNo auth
Up to 20 captures in one request, sharing one browser instance. Each entry takes the same options as a single capture.

Cost 1 image credit per capture in the batch.

Body parameters

items
object[]required
Array of capture option objects.
presetId
stringoptional
Applies a saved preset to every item in the batch — the usual way to capture many URLs with one shared recipe. An item may carry its own presetId, which wins for that item.
defaults
objectoptional
Options merged into every item, applied over the preset and under each item’s own options.
{
  "presetId": "prs_9d2f",
  "items": [
    {
      "url": "https://example.com"
    },
    {
      "url": "https://example.org",
      "fullPage": true
    }
  ]
}

Try it

/api/v1/screenshot/batch
Show as cURL
curl -X POST "https://pixbix.app/api/v1/screenshot/batch" \
  -H "x-api-key: pk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "presetId": "prs_9d2f", "items": [ { "url": "https://example.com" }, { "url": "https://example.org", "fullPage": true } ] }'

List saved presets

GET/api/v1/screenshot/presetsNo auth
Every capture preset saved in this workspace, by name. Use an id as `presetId` on a capture to apply the whole saved option set.

Query parameters

page
numberoptional
Page number, from 1.
limit
numberoptional
Rows per page, 1–100. Defaults to 100.
sort
stringoptional
Column to order by: name, createdAt, updatedAt. Defaults to `name`.
order
"asc" | "desc"optional
Sort direction. Defaults to `desc` for dates and counts.
search
stringoptional
Case-insensitive match across name. Up to 128 characters.
Response
{
  "success": true,
  "data": [
    {
      "id": "prs_9d2f",
      "name": "Pricing page — retina",
      "options": {
        "url": "https://acme.in/pricing",
        "format": "png",
        "fullPage": true,
        "scale": 2,
        "viewport": {
          "width": 1440,
          "height": 900
        },
        "blockCookieBanners": true,
        "hideSelectors": [
          "#intercom-container"
        ]
      },
      "updatedAt": "2026-08-14T11:02:19.441Z"
    }
  ]
}

Try it

/api/v1/screenshot/presets?page=1&limit=100&sort=name&order=desc&search=diwali
Show as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot/presets?page=1&limit=100&sort=name&order=desc&search=diwali" \
  -H "x-api-key: pk_live_your_key"

Get a preset

GET/api/v1/screenshot/presets/:idNo auth
One preset and the exact options it holds — useful for inspecting what a `presetId` will apply before you rely on it.

Path parameters

id
stringrequired
Preset id.
Response
{
  "success": true,
  "data": {
    "id": "prs_9d2f",
    "name": "Pricing page — retina",
    "options": {
      "url": "https://acme.in/pricing",
      "format": "png",
      "fullPage": true,
      "scale": 2
    },
    "createdAt": "2026-08-01T08:15:00.000Z",
    "updatedAt": "2026-08-14T11:02:19.441Z"
  }
}

Try it

/api/v1/screenshot/presets/prs_9d2f
Show as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot/presets/prs_9d2f" \
  -H "x-api-key: pk_live_your_key"

Save a preset

POST/api/v1/screenshot/presetsNo auth
Creates a preset, or updates the existing one with the same name — names are unique per workspace, so saving twice edits rather than duplicating. Options are validated before they are stored, so a preset that cannot be captured is rejected here rather than failing later on reuse.

Body parameters

name
stringrequired
Up to 60 characters. Unique within the workspace.
options
objectrequired
Any capture options, exactly as POST /v1/screenshot accepts them. Delivery fields (response, download, filename) are stripped — those are decided per capture, not saved.
{
  "name": "Pricing page — retina",
  "options": {
    "url": "https://acme.in/pricing",
    "format": "png",
    "fullPage": true,
    "scale": 2,
    "viewport": {
      "width": 1440,
      "height": 900
    },
    "blockCookieBanners": true,
    "hideSelectors": [
      "#intercom-container"
    ]
  }
}

Saving writes a preset into your workspace, so it is not fired from the docs.

Delete a preset

DELETE/api/v1/screenshot/presets/:idNo auth
Removes a saved preset. Captures already taken are unaffected; jobs still naming this id will start failing with PRESET_NOT_FOUND.

Path parameters

id
stringrequired
Preset id.
Response
{
  "success": true,
  "message": "Preset deleted",
  "data": {
    "id": "prs_9d2f"
  }
}

Deleting is destructive — run it against a preset id you own from your own client.

Options reference

GET/api/v1/screenshot/optionsNo auth
Returns the full, live option set this deployment accepts, as JSON. Open — it needs no API key, so it is safe to call from anywhere.
Response
{
  "success": true,
  "data": {
    "formats": [
      "png",
      "jpeg",
      "webp",
      "avif",
      "pdf"
    ],
    "maxBatch": 20
  }
}

Try it

/api/v1/screenshot/options
Show as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot/options" \
  -H "x-api-key: pk_live_your_key"

Renderer status

GET/api/v1/screenshot/statusNo auth
Health of the render pool: Chromium shards, pages, queue depth and the caches in front of them. `renderCaches.render.hitRate` is the share of image renders served without any compute at all — the number to watch when sizing capacity, and a sudden drop usually means templates changed rather than that traffic grew. `renderQueue.role` says whether this replica renders or dispatches; a non-zero `renderQueue.breakerOpenMs` means it cannot reach the queue and is rendering locally, `fallbacks` counts how often that has happened, and `renderQueue.lastError` gives the reason Redis refused — the fastest way to tell a wrong password from an unreachable host. Template renders, URL captures and design imports all go through that queue, so on a dispatching replica `renderer.connected` stays false — no browser is ever launched on the tier serving your users.
Response
{
  "success": true,
  "data": {
    "renderer": {
      "connected": true,
      "remote": false,
      "shards": 2,
      "shardsUp": 2,
      "active": 3,
      "queued": 0,
      "peakQueued": 12,
      "shed": 0,
      "maxConcurrency": 8,
      "queueTimeoutMs": 30000,
      "pages": {
        "idle": 5,
        "open": 8,
        "maxUses": 50,
        "poolSize": 4,
        "created": 9,
        "reused": 1841,
        "retired": 1
      }
    },
    "cache": {
      "entries": 12,
      "bytes": 8419320,
      "hits": 96,
      "misses": 41
    },
    "renderQueue": {
      "enabled": true,
      "role": "api",
      "name": "pixbix-stills",
      "workerRunning": false,
      "concurrency": 4,
      "waitTimeoutMs": 30000,
      "breakerOpenMs": 0,
      "lastError": null,
      "queued": 18412,
      "local": 0,
      "tooLarge": 3,
      "cacheHits": 11208,
      "timeouts": 0,
      "fallbacks": 0
    },
    "renderCaches": {
      "render": {
        "enabled": true,
        "entries": 734,
        "bytes": 511300284,
        "hitRate": 0.62,
        "hits": 1204,
        "misses": 734,
        "evictions": 0
      },
      "fonts": {
        "enabled": true,
        "families": 11,
        "bytes": 24118400,
        "hits": 1932,
        "misses": 11,
        "diskHits": 4,
        "failures": 0
      },
      "assets": {
        "enabled": true,
        "entries": 218,
        "bytes": 194883072,
        "hits": 1611,
        "misses": 218,
        "failures": 2,
        "blocked": 2
      }
    },
    "limits": {
      "maxDimension": 6000,
      "maxScale": 3,
      "timeoutMs": 30000
    },
    "batchMax": 10,
    "storage": "s3"
  }
}

Try it

/api/v1/screenshot/status
Show as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot/status" \
  -H "x-api-key: pk_live_your_key"

Clear the capture cache

POST/api/v1/screenshot/cache/clearNo auth
Drops every cached capture so the next request for a URL re-renders it. Use after deploying a page whose screenshots are being served stale.
Response
{
  "success": true,
  "message": "Cache cleared",
  "data": {
    "cleared": 128
  }
}

Clearing the cache affects every caller on this deployment, so it is not fired from the docs.

Every endpoint#

The rest of the public API, grouped by what it renders. Endpoints outside this page link to the reference that documents them.

Postman#

Generated from the same catalogue as this page. In Postman choose Import → Link and paste either URL.

Collection

Every public v1 endpoint, foldered by API, plus webhook management — with example bodies and saved responses.

https://pixbix.app/postman/collection.jsonOpen JSON

Environment

baseUrl plus empty, secret-typed apiKey and authToken. Fill the credentials in Postman, not here.

https://pixbix.app/postman/environment.jsonOpen JSON