Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Chrome DevTools Protocol (CDP) to automate a browser that visits each URL, captures the rendered page with Page.captureScreenshot, decodes its base64 response, and saves one PNG per URL. CDP handles each navigation and capture; your script supplies the URL loop, readiness checks, filenames, and error handling.

What the workflow does

For each URL, your automation attaches to a Chrome or Chromium page target, enables the Page domain, navigates with Page.navigate, waits for the page state you need, then calls Page.captureScreenshot with PNG output. The screenshot response contains base64 image data, which must be decoded before it is written as a binary .png file. See the Chrome DevTools Protocol Page domain reference for method parameters and response fields.

The protocol gives you control over browser-level operations, but it does not provide a universal definition of “ready.” A page may still be loading application data or changing after navigation events have fired. Select a readiness condition that fits the sites you capture.

Capture a URL list directly through CDP

One practical way to call CDP methods is through Puppeteer’s CDP session API. The example below uses Node.js, connects to a Chrome instance exposing its remote debugging endpoint, and captures URLs sequentially. Sequential capture makes it easier to associate a navigation or file error with the URL that caused it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prerequisites and setup

  • Install a current Node.js release and Puppeteer: npm install puppeteer.
  • Start Chrome or Chromium with remote debugging enabled, then provide its debugging WebSocket URL in CDP_WS_ENDPOINT. Protect this endpoint: anyone who can connect may be able to control the browser.
  • Set SCREENSHOT_URLS to a comma-separated list of URLs. The output directory is created automatically.

Runnable Node.js example

const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');

const endpoint = process.env.CDP_WS_ENDPOINT;
const urls = (process.env.SCREENSHOT_URLS || '')
  .split(',')
  .map((url) => url.trim())
  .filter(Boolean);
const outputDir = process.env.OUTPUT_DIR || 'screenshots';

if (!endpoint) throw new Error('Set CDP_WS_ENDPOINT to Chrome’s debugging WebSocket URL.');
if (urls.length === 0) throw new Error('Set SCREENSHOT_URLS to a comma-separated URL list.');

function filenameFor(url, index) {
  let host = 'page';
  try { host = new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_'); } catch {}
  return `${String(index + 1).padStart(3, '0')}-${host}.png`;
}

(async () => {
  await fs.mkdir(outputDir, { recursive: true });
  const browser = await puppeteer.connect({ browserWSEndpoint: endpoint });
  const page = await browser.newPage();
  const session = await page.createCDPSession();
  await session.send('Page.enable');

  const failures = [];
  try {
    for (const [index, url] of urls.entries()) {
      const file = path.join(outputDir, filenameFor(url, index));
      try {
        const navigation = await session.send('Page.navigate', { url });
        if (navigation.errorText) throw new Error(navigation.errorText);

        // This is a baseline, not proof that app-specific content is ready.
        await page.waitForFunction(() => document.readyState === 'complete', { timeout: 30000 });
        // Replace/add an application-specific wait when the page renders dynamically.
        const result = await session.send('Page.captureScreenshot', { format: 'png' });
        await fs.writeFile(file, Buffer.from(result.data, 'base64'));
        console.log(`Saved ${url} -> ${file}`);
      } catch (error) {
        failures.push({ url, file, error: error.message });
        console.error(`Failed ${url}: ${error.message}`);
      }
    }
  } finally {
    await session.detach().catch(() => {});
    await page.close().catch(() => {});
    await browser.disconnect();
  }

  if (failures.length) {
    console.error('Failures:', failures);
    process.exitCode = 1;
  }
})();

Set the environment variables before running the script. For example, on macOS or Linux:

export CDP_WS_ENDPOINT='ws://127.0.0.1:9222/devtools/browser/REPLACE_WITH_BROWSER_ID'
export SCREENSHOT_URLS='https://example.com,https://example.org'
node capture-list.js

The endpoint shown is illustrative; use the actual WebSocket URL exposed by the Chrome instance you started. Do not expose a remote-debugging port to an untrusted network.

Rank #2
Free Fling File Transfer Software for Windows [PC Download]
  • Intuitive interface of a conventional FTP client
  • Easy and Reliable FTP Site Maintenance.
  • FTP Automation and Synchronization

Important navigation details

Page.navigate returns a frame identifier and may return a loaderId; same-document navigations can omit that identifier. When navigation fails, the response can include errorText. The example checks this field and records failures per URL rather than silently saving a screenshot as though navigation succeeded. It also checks the document’s ready state, but single navigation events or a completed document state are not a guarantee that a JavaScript application has finished rendering.

For a dynamic page, wait for a stable, page-specific signal before capture—for example, a known results container or a loading indicator disappearing. If you capture authenticated pages, configure the browser session with the required login state before navigating. CDP itself does not sign in for you.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the screenshot area and format

Page.captureScreenshot supports png, jpeg, and webp; PNG is the documented default. The method’s clip option can capture a specified region. Decide whether you need the visible viewport, a clipped area, or content beyond it, and validate the result against the target pages. captureBeyondViewport is documented as experimental, so check the protocol version supported by the Chrome you are actually using before depending on it.

The example intentionally captures the page’s current browser screenshot state without a clip. It does not promise a full-page image: choose and test the appropriate capture behavior for your Chrome version and use case rather than assuming content outside the viewport will be included.

Make the batch reliable

Use stable, unique filenames

The sample prefixes filenames with the list position and hostname, so two URLs on the same host do not overwrite one another. For repeated runs, consider adding a timestamp or organizing output by run. If URLs may contain sensitive query values, avoid placing the full URL in filenames or logs.

Keep failures isolated

Navigation errors, readiness timeouts, screenshot errors, and filesystem failures can affect individual URLs. The loop records each failure and proceeds to the next URL; it exits with a nonzero status at the end if any capture failed. This lets a scheduler or calling process detect an incomplete batch while retaining successful files.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle readiness and consistency deliberately

  • Use a selector or application signal when the screenshot depends on asynchronously loaded content.
  • Use a consistent viewport and browser state across captures if you need comparable screenshots.
  • Test lazy-loaded pages and long pages specifically; the protocol reference does not establish a single universal full-page behavior or readiness timeout.
  • Do not infer a universal concurrency limit or capture speed. The protocol documentation does not specify one; measure your own workload before adding parallel tabs or browsers.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Direct CDP or Puppeteer?

Approach Best fit Trade-off
Direct CDP methods You need explicit control over protocol methods and fields such as Page.navigate and screenshot options. You must manage the session, navigation results, readiness, base64 decoding, output files, and errors yourself.
Puppeteer Your team already uses Node.js browser automation and wants convenient page navigation and screenshot-file workflows. It is a higher-level library; exact protocol options and experimental fields still depend on the Chrome version and CDP support.

Puppeteer’s official guides document browser pages and its screenshot workflow: Puppeteer screenshots and Puppeteer Browser API. The example above uses Puppeteer to connect to Chrome while issuing the navigation and screenshot commands through a CDP session.

Common errors and fixes

  • Cannot connect to browser: confirm Chrome is running with remote debugging enabled and that CDP_WS_ENDPOINT is the browser WebSocket endpoint, not a page target URL or an example placeholder.
  • Navigation response contains errorText: the browser reported a navigation failure. Check the URL, network access, redirects, and whether the destination is reachable from the machine running Chrome; retain the URL and error for a targeted retry.
  • Screenshot is blank or incomplete: capture may have occurred before the application rendered its content. Replace the generic ready-state wait with a selector or other site-specific readiness condition, and inspect the page in the same browser session.
  • Screenshot file cannot be written: check that the output path is writable and that there is sufficient space. The script creates the directory, but it cannot fix permission or storage errors.
  • Same-page navigation behaves differently: same-document navigation may not include a loaderId. Do not treat its absence alone as a failed navigation; inspect errorText and wait for the page condition relevant to the capture.
  • Experimental option is rejected: verify that the connected Chrome’s protocol version supports the parameter. In particular, captureBeyondViewport is experimental in the reference.

Or skip the browser setup

ScreenshotNeo offers a one-request screenshot API and an MCP server for AI agents. A GET request can return a PNG, JPEG, WebP, or PDF; its documentation is at ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Before the capture, ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response indicates the page verdict and billing status. AI agents can use its MCP server tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to try 1,000 screenshots a month with no card.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I save the CDP screenshot response directly as a PNG?

No. Decode the response’s base64 `data` value to binary bytes before writing the `.png` file.

Does CDP wait until every dynamic element has loaded?

No. Readiness depends on the site and on what the capture must show; wait for an application-specific condition when necessary.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.