Cloudflare Browser Rendering: How to Capture Website Screenshots
Capture website screenshots with Cloudflare Browser Rendering: configure the API, choose wait and viewport options, save the image, and troubleshoot common failures.
To capture a website screenshot with Cloudflare Browser Rendering, send an authenticated POST request to the account-specific /browser-rendering/screenshot endpoint. If you want to receive the screenshot together with page content, use the separate /browser-rendering/snapshot endpoint and request the screenshot format. Cloudflare documents the snapshot result’s screenshot as a base64-encoded image. The examples below use snapshot so they can decode and save the image consistently. See Cloudflare’s Browser Rendering API reference and snapshot create reference.
1. Prepare Cloudflare access
- Find your Cloudflare account ID in the dashboard.
- Create an API token with the Browser Rendering Write permission. Cloudflare recommends API tokens and bearer authentication.
- Keep the token on the server or in your local environment. Do not expose it in client-side JavaScript or commit it to source control.
Set these environment variables in your shell, replacing the placeholders:
export CF_ACCOUNT_ID="your_account_id"
export CLOUDFLARE_API_TOKEN="your_browser_rendering_token"
2. Capture a page and save the screenshot
The snapshot endpoint accepts JSON. This request asks Cloudflare for a screenshot and page content, waits for the page’s load event, and writes the decoded screenshot bytes to shot.png. The formats field controls which result fields are requested.
cURL
curl --fail-with-body --silent --show-error \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/browser-rendering/snapshot" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"url": "https://example.com",
"formats": ["screenshot", "content"],
"gotoOptions": {
"waitUntil": "load",
"timeout": 60000
},
"screenshotOptions": {
"type": "png",
"fullPage": true
}
}' \
-o response.json
python3 -c 'import base64,json; d=json.load(open("response.json")); assert d.get("success"), d.get("errors"); open("shot.png","wb").write(base64.b64decode(d["result"]["screenshot"]))'
Snapshot options are documented by Cloudflare and can change. Confirm the current schema before relying on less common fields such as screenshot output type or clip settings.
Python
Install the dependency with python -m pip install requests. This script checks HTTP errors and Cloudflare’s JSON-level success flag before saving the base64 image.
import base64
import os
import requests
account_id = os.environ["CF_ACCOUNT_ID"]
token = os.environ["CLOUDFLARE_API_TOKEN"]
endpoint = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/snapshot"
payload = {
"url": "https://example.com",
"formats": ["screenshot", "content"],
"gotoOptions": {"waitUntil": "load", "timeout": 60000},
"screenshotOptions": {"type": "png", "fullPage": True},
}
response = requests.post(
endpoint,
headers={"Authorization": f"Bearer {token}"},
json=payload,
timeout=90,
)
response.raise_for_status()
data = response.json()
if not data.get("success"):
raise RuntimeError(f"Cloudflare Browser Rendering failed: {data.get('errors')}")
image_b64 = data.get("result", {}).get("screenshot")
if not image_b64:
raise RuntimeError(f"No screenshot in response: {data}")
with open("shot.png", "wb") as image_file:
image_file.write(base64.b64decode(image_b64))
print("Saved shot.png")
Node.js
This uses Node.js built-in fetch and Buffer. It runs in a server or local script with the two environment variables set.
const accountId = process.env.CF_ACCOUNT_ID;
const token = process.env.CLOUDFLARE_API_TOKEN;
if (!accountId || !token) throw new Error("Set CF_ACCOUNT_ID and CLOUDFLARE_API_TOKEN");
const endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/snapshot`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com",
formats: ["screenshot", "content"],
gotoOptions: { waitUntil: "load", timeout: 60000 },
screenshotOptions: { type: "png", fullPage: true },
}),
signal: AbortSignal.timeout(90000),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(`Cloudflare request failed (${response.status}): ${JSON.stringify(data.errors ?? data)}`);
}
const screenshot = data.result?.screenshot;
if (!screenshot) throw new Error("Response did not contain result.screenshot");
await import("node:fs/promises").then(({ writeFile }) => writeFile("shot.png", Buffer.from(screenshot, "base64")));
console.log("Saved shot.png");
3. Choose screenshot or snapshot
Cloudflare exposes distinct routes for a direct screenshot and a snapshot. Use the direct screenshot operation when the job is only to capture an image; use snapshot when your application also needs HTML or other supported page outputs. The snapshot endpoint can return HTML content and a screenshot, and its screenshot value is base64 encoded. Do not assume both routes have identical request or response schemas: consult the current endpoint reference when switching routes.
| Need | Route | What to handle |
|---|---|---|
| Screenshot capture | POST /accounts/{account_id}/browser-rendering/screenshot |
Follow the screenshot endpoint’s current request and response schema. |
| Screenshot plus page data | POST /accounts/{account_id}/browser-rendering/snapshot |
Request screenshot in formats; decode result.screenshot from base64. |
4. Configure the capture
For snapshot requests, Cloudflare documents navigation controls through gotoOptions and waitFor* settings, and screenshot customization such as viewport, fullPage, and clip. Check the current API schema for accepted fields and types before adding options; fields and allowed values can differ between routes.
Navigation timing
gotoOptions.waitUntil can use load, domcontentloaded, networkidle0, or networkidle2. Select a condition based on the page:
domcontentloadedis useful when the initial document is enough, but client-rendered content may still be missing.loadwaits for the load event and is a reasonable general starting point.networkidle0andnetworkidle2wait for network activity to settle. Pages with persistent polling or analytics may not settle promptly.- For content that appears after navigation, use a documented
waitFor*condition or an explicit delay where appropriate. Prefer waiting for a meaningful page condition to adding a large fixed delay.
The API reference documents a navigation timeout up to 60,000 milliseconds for gotoOptions.timeout. Browser actions after page load also have a documented maximum duration of 120,000 milliseconds. Treat those values as API limits, not a promise that every page completes in that time.
Viewport, full page, and clip
- Viewport capture: captures the visible browser area at the configured viewport dimensions.
- Full page: set
fullPagewhen you need the entire document. Very long pages can take longer and produce large images. - Clip: use a clip rectangle when only a particular region is needed. Verify the coordinates and dimensions against the viewport and current API schema.
- Viewport: set explicit width and height when layout consistency matters; responsive pages can render differently at different widths.
Output format and response decoding
The screenshot options document PNG, JPEG, and WebP output where supported. With snapshot, the screenshot is returned as base64 in JSON; decode it before writing the file. The JSON wrapper is not itself an image. For JPEG output, use a .jpg filename; for WebP, use .webp. Validate the current route’s option schema before changing the example’s PNG setting.
5. Handle errors and make captures reliable
Check both the HTTP status and the JSON response’s success field. A request can return an API error object even when your HTTP client successfully parsed the response. The snapshot reference shows an error response for rate limiting, for example. Log the Cloudflare error code and message, but never log the bearer token.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 or 403 | Missing, invalid, expired, or insufficiently scoped token. | Use a valid bearer token with Browser Rendering Write permission and verify the account ID. |
| Rate-limit error or HTTP 429 | Requests exceed the applicable account or plan rate. | Reduce concurrency, queue work, and retry with exponential backoff and jitter. Check current dashboard limits. |
| Timeout | The page is slow, a wait condition never settles, or browser work exceeds its allowed duration. | Use a suitable wait condition, wait for a specific condition, inspect the target URL, and avoid unnecessarily large full-page captures. |
| Blank or incomplete image | Capture occurred before client-rendered content, fonts, or images were ready. | Wait for the relevant selector or page condition; use a longer delay only if there is no reliable condition available. |
No result.screenshot |
The request did not ask for the screenshot format, the API returned an error, or the response schema differs for the selected endpoint. | Check success and errors, request screenshot in snapshot formats, and verify endpoint documentation. |
| Saved file cannot be opened | JSON was saved as an image, base64 was not decoded, or the extension does not match the image type. | Decode the base64 screenshot bytes and use a matching extension. Do not save the whole JSON response as PNG. |
| Different layout between runs | Viewport, page state, dynamic content, or timing differs. | Set a stable viewport and wait condition; use a page state that can be reproduced. |
Retries and repeatability
- Retry transient network errors, timeouts, and rate-limit responses with bounded exponential backoff and jitter. Do not retry authentication or malformed-request errors unchanged.
- Set a maximum attempt count and an overall deadline so a stuck URL cannot hold a worker indefinitely.
- Record the requested URL, capture options, response status, and Cloudflare error details. Avoid recording secrets or sensitive page content.
- For recurring visual checks, keep viewport, timing, and output options constant. Dynamic page data can still make screenshots differ.
6. Performance, reliability, and cost
Rendering a page requires browser work and network loading, so total time depends on the target site, assets, chosen wait condition, and capture size. A full-page image can require more work and memory than a viewport image. Request only the output formats you need and avoid waiting for network idle on pages that keep connections open.
Plan for rate limits and transient failures: throttle concurrent requests, use bounded retries, and monitor errors and usage in the Cloudflare dashboard. Cloudflare’s March 4, 2026 changelog reports a Workers Paid REST API limit of 10 requests per second (600 per minute), increased from 3 requests per second (180 per minute). Actual account limits depend on plan and can change.
Cloudflare’s July 28, 2025 pricing announcement stated that Workers Free included 10 minutes of Browser Rendering per day and up to 3 concurrent browsers; Workers Paid included 10 hours per month and an average of 10 concurrent browsers, with additional REST API browser time listed at $0.09 per browser hour and additional concurrency at $2.00 per concurrent browser. Those are figures from the dated announcement, not a guarantee of current terms. Check the current announcement and your dashboard before estimating spend: Cloudflare Browser Rendering pricing announcement and REST API rate-limit changelog.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, and failed loads are never billed.
- An MCP server lets AI agents use screenshots through Claude, Cursor, or any MCP client.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, no card required.
FAQ
Can I get HTML and a screenshot from one request?
Yes. Use the snapshot endpoint and request both content and screenshot formats.
Why is the snapshot image a string instead of binary data?
The screenshot is base64 encoded inside the JSON result. Decode that value to obtain the image file bytes.
Which wait condition should I use?
Start with load for general pages. Choose a selector or another condition when the visible content is rendered later; persistent network activity can make network-idle waits unsuitable.
Does full-page capture guarantee every lazy-loaded image appears?
No such guarantee is established by the cited API reference. If the site loads images only as the page scrolls, inspect the result and use the documented controls appropriate to the target page.


