How to Capture a Website Screenshot with Browserless and Node.js
Capture a website with Browserless from Node.js, save the image bytes correctly, and choose the right options for full pages, lazy content, and browser sessions.
For a one-off website screenshot from Node.js, send a JSON POST request to Browserless’s /screenshot endpoint, pass your token in the query string, then save the response as binary image data. Set options.fullPage to true when you need the whole document.
Capture a page with the Browserless REST API
The example below uses Node.js’s built-in fetch and writes the returned image bytes to screenshot.png. Create a Browserless token and put it in the BROWSERLESS_TOKEN environment variable before running the script.
import fs from 'node:fs/promises';
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set BROWSERLESS_TOKEN');
const endpoint = new URL('https://production-sfo.browserless.io/screenshot');
endpoint.searchParams.set('token', token);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com',
options: { fullPage: true, type: 'png' },
}),
});
if (!response.ok) {
throw new Error(`Screenshot request failed: ${response.status} ${response.statusText}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
await fs.writeFile('screenshot.png', bytes);
console.log('Saved screenshot.png');
Save this as screenshot.mjs, then run:
export BROWSERLESS_TOKEN='your-token'
node screenshot.mjs
Keep the token out of source control, logs, and client-side code. The endpoint returns image bytes, not JSON, so do not call response.json(). Browserless’s [REST screenshot documentation](https://docs.browserless.io/rest-apis/screenshot) describes the endpoint and request options; consult its current reference for accepted option values.
Choose the capture options
Browserless screenshot settings go in the JSON request body. The documented shape is a target url and an options object. These commonly relevant choices control what part of the page is captured and how the output is encoded.
| Need | Setting | Notes |
|---|---|---|
| Capture the full document | options.fullPage: true |
Captures beyond the initial viewport. Very long pages can produce large images. |
| Capture one element | Top-level selector |
The screenshot API waits for the element and crops to its bounds. Missing or late elements can cause a wait failure. |
| Capture a fixed rectangle | options.clip |
Use a clip rectangle when you know the viewport region to capture. |
| Choose an image format | options.type |
PNG, JPEG, and WebP are documented formats. Check the current API reference for accepted spelling and values. |
| Adjust lossy image size | options.quality |
Quality applies to lossy formats such as JPEG and WebP; it does not affect PNG. |
| Wait for images | options.waitForImages |
Useful when the page’s images must finish loading before the capture. |
| Allow page navigation time | options.timeout |
Set an appropriate limit for slower pages, subject to the service’s current limits. |
For example, change the body to request a WebP full-page image:
body: JSON.stringify({
url: 'https://example.com',
options: { fullPage: true, type: 'webp', quality: 80 },
}),
For an element screenshot, the selector is a top-level field in the documented REST request shape:
body: JSON.stringify({
url: 'https://example.com',
selector: 'main article',
options: { type: 'png' },
}),
Option placement matters. Browserless REST and BrowserQL are distinct interfaces; do not assume that a field documented for one has the same location or support in the other. See the [screenshot REST API reference](https://docs.browserless.io/rest-apis/screenshot) and [screenshot mutation reference](https://docs.browserless.io/browserql/using-the-ide/screenshot) for the specific interface you use.
Handle full pages and lazy-loaded content
fullPage: true requests the full page, but some sites only load images or sections after they enter the viewport. For those pages, Browserless documents scrollPage: true to scroll before the capture; combine it with full-page mode when capturing a long document.
body: JSON.stringify({
url: 'https://example.com/articles/long-page',
options: { fullPage: true, scrollPage: true, type: 'png' },
}),
Scrolling can trigger additional network requests and make a capture slower. If the page has an endless feed or continuously loading content, decide what region or stopping point you need instead of assuming the document will settle on its own.
When to use a browser connection instead of REST
Use the REST endpoint when a single navigation followed by a screenshot is enough. Browserless describes REST as a fit for stateless, one-shot work. Use a managed Puppeteer or Playwright connection when you need to click through a flow, set up page state, or coordinate custom waits before capturing.
Puppeteer over Browserless WebSocket
Install puppeteer-core, which connects to the remote browser without downloading a local Chromium browser:
npm install puppeteer-core
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer-core';
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set BROWSERLESS_TOKEN');
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io?token=${encodeURIComponent(token)}`,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
const image = await page.screenshot({ fullPage: true, type: 'png' });
await fs.writeFile('screenshot.png', image);
} finally {
await browser.close();
}
Always close the managed browser session when finished so it is released. For Playwright, use Browserless’s documented Chromium Playwright endpoint and close the browser/session in a finally block as well. See Browserless’s [Puppeteer guide](https://docs.browserless.io/baas/puppeteer) and [Playwright guide](https://docs.browserless.io/baas/playwright) for current connection details.
Request the screenshot with cURL or Python
The same REST request can be made without Node.js. These alternatives are useful for debugging the endpoint or incorporating it into another script.
cURL
curl -X POST \
"https://production-sfo.browserless.io/screenshot?token=${BROWSERLESS_TOKEN}" \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
--data '{"url":"https://example.com","options":{"fullPage":true,"type":"png"}}' \
--output screenshot.png
Python
import os
import requests
token = os.environ.get("BROWSERLESS_TOKEN")
if not token:
raise RuntimeError("Set BROWSERLESS_TOKEN")
response = requests.post(
"https://production-sfo.browserless.io/screenshot",
params={"token": token},
headers={
"Cache-Control": "no-cache",
"Content-Type": "application/json",
},
json={
"url": "https://example.com",
"options": {"fullPage": True, "type": "png"},
},
timeout=90,
)
response.raise_for_status()
with open("screenshot.png", "wb") as image_file:
image_file.write(response.content)
Remove the accidental leading space before token if copying into a strict formatter? In Python, indentation at module scope is invalid. Use the corrected line token = os.environ.get("BROWSERLESS_TOKEN") aligned with import.
Or skip the browser setup
If you only need a screenshot from a URL, [ScreenshotNeo](https://screenshotneo.com) offers a one-call API. Use the documented [API reference](https://screenshotneo.com/docs/) for its parameters and response behavior:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| 401 or 403 response | Missing, invalid, or unauthorized token; token may also be malformed in the URL. | Check BROWSERLESS_TOKEN, URL-encode it, and confirm it is valid for the Browserless endpoint and plan you are using. |
| HTML or JSON error saved as a PNG | The code wrote an error response body without checking the HTTP status. | Check response.ok before saving and inspect the error response separately. Do not parse a successful screenshot response as JSON. |
| Image file is empty or truncated | The response was not fully read, or an upstream error was saved as if it were an image. | Use arrayBuffer() and a binary write, verify the response status, and ensure the destination directory is writable. |
| Blank screenshot | The destination returned a blank page, failed to render, or blocked automated access. | Check the target URL in a normal browser and inspect whether it presents a bot check or access-denied page. Browserless documents an /unblock API for certain anti-bot cases, but it cannot guarantee access to every site. |
| Missing image or content lower on the page | Lazy loading only starts when content is scrolled into view. | Try scrollPage: true together with fullPage: true, and allow enough time for the content to load. |
| Element screenshot fails | The selector does not match, or the element appears after the wait limit. | Confirm the selector against the rendered page, use a selector that identifies one intended element, and adjust the timeout if appropriate. |
| Request takes too long | The site is slow, has long-running network activity, or loads a very large page. | Choose a suitable timeout and capture a smaller region or viewport when full-page output is unnecessary. For custom interaction and waits, use a browser connection. |
Performance, reliability, and cost considerations
- Keep captures bounded. Full-page screenshots of very long documents use more time and produce larger files than viewport or element captures. Prefer the smallest area that satisfies the task.
- Choose the output deliberately. PNG preserves lossless image data; JPEG and WebP can reduce output size with a quality setting. Check downstream compatibility before selecting a lossy format.
- Use REST for isolated jobs. A single HTTP request has no browser session for your code to manage. Interactive flows require a remote browser connection and explicit cleanup.
- Handle transient failures. Check status codes and report the target URL and request context in safe logs. Avoid logging the token. If adding retries, use a bounded retry policy for transient failures; repeated retries cannot fix a blocked destination or invalid request.
- Review current service limits and pricing. Browserless capacity, timeout limits, and account pricing are service-specific and can change. Consult the current Browserless account and API documentation before estimating production volume.
Frequently asked questions
Does this require Puppeteer?
No. The REST screenshot endpoint needs only an HTTP request. Puppeteer or Playwright is useful when the page must be manipulated before capture.
Can Browserless return a full-page screenshot?
Yes. The documented REST option is options.fullPage: true.
Why is the token in the URL?
The documented REST endpoint accepts the Browserless token as a token query parameter. Keep it in an environment variable and avoid exposing request URLs in logs.
Will it capture every website?
No service can infer or bypass every site’s access controls. A bot challenge, CAPTCHA, or access-denied response may be what the browser renders instead of the requested page.
Can I capture only one part of a page?
Yes. Use the documented top-level selector for an element, or options.clip for a fixed rectangle.


