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.
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.
{
"presetId": "prs_9d2f",
"url": "https://acme.in/enterprise",
"viewport": { "width": 390 }
}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.
{
"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.
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"
}
}'| Option | Meaning |
|---|---|
key | What the value is called in `extracted`. Letters, numbers and underscores. |
selector | CSS selector of the element to read. |
attribute | Read an attribute instead of the text — href, src, content. |
all | Return every match as an array rather than the first as a string. |
index | Read 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. |
layerId | Image templates: the layer this value fills — text, a picture, or a data element. |
layerField | Which 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
/api/v1/screenshotNo authCost 1 image credit per capture.
Body parameters
urlstringoptional- Page to capture. Provide either url or html.
htmlstringoptional- Raw HTML to render instead of fetching a URL.
presetIdstringoptional- 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.
fullPagebooleanoptional- Capture the entire scroll height.
viewport.widthnumberoptional- Defaults to 1280.
viewport.heightnumberoptional- Defaults to 800.
selectorstringoptional- Capture only the first element matching this CSS selector.
waitUntilstringoptional- load, domcontentloaded, networkidle0 or networkidle2.
cookiesarrayoptional- Cookies to set before navigating, for pages behind a login: [{ name, value, domain?, path?, httpOnly?, secure?, expires? }]. domain defaults to the host of url.
authenticateobjectoptional- 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.
extractarray | 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/screenshotShow 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
/api/v1/screenshotNo authCost 1 image credit per capture, unless served from cache.
Query parameters
presetIdstringoptional- Apply a saved preset as the baseline. Any other parameter you send overrides it.
urlstringrequired- Page to capture.
formatstringoptional- Defaults to png.
fullPagebooleanoptional- Capture the entire scroll height.
{
"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=trueShow 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
/api/v1/screenshot/batchNo authCost 1 image credit per capture in the batch.
Body parameters
itemsobject[]required- Array of capture option objects.
presetIdstringoptional- 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.
defaultsobjectoptional- 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/batchShow 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
/api/v1/screenshot/presetsNo authQuery parameters
pagenumberoptional- Page number, from 1.
limitnumberoptional- Rows per page, 1–100. Defaults to 100.
sortstringoptional- Column to order by: name, createdAt, updatedAt. Defaults to `name`.
order"asc" | "desc"optional- Sort direction. Defaults to `desc` for dates and counts.
searchstringoptional- Case-insensitive match across name. Up to 128 characters.
{
"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=diwaliShow 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
/api/v1/screenshot/presets/:idNo authPath parameters
idstringrequired- Preset id.
{
"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_9d2fShow 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
/api/v1/screenshot/presetsNo authBody parameters
namestringrequired- Up to 60 characters. Unique within the workspace.
optionsobjectrequired- 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
/api/v1/screenshot/presets/:idNo authPath parameters
idstringrequired- Preset id.
{
"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
/api/v1/screenshot/optionsNo auth{
"success": true,
"data": {
"formats": [
"png",
"jpeg",
"webp",
"avif",
"pdf"
],
"maxBatch": 20
}
}Try it
/api/v1/screenshot/optionsShow as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot/options" \ -H "x-api-key: pk_live_your_key"
Renderer status
/api/v1/screenshot/statusNo auth{
"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/statusShow as cURL
curl -X GET "https://pixbix.app/api/v1/screenshot/status" \ -H "x-api-key: pk_live_your_key"
Clear the capture cache
/api/v1/screenshot/cache/clearNo auth{
"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 JSONEnvironment
baseUrl plus empty, secret-typed apiKey and authToken. Fill the credentials in Postman, not here.
https://pixbix.app/postman/environment.jsonOpen JSON