Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Markdown to Image Dark Mode Image Format & Quality MCP Server Webhook Screenshot to Base64 Transparent Background Email to Image Certified Screenshot Geo Screenshot Stealth Mode CSS & JS Injection Block Ads & Chat Widgets Pricing Docs Blog Log In Sign Up

Stealth Screenshot API

About half of all websites with bot protection run Cloudflare. When you try to screenshot one of these sites with a regular headless browser, you almost never get the real page. What comes back is a Cloudflare challenge screen ("Checking your browser..."), a CAPTCHA, or just a blank page. The request returns HTTP 200 and saves an image file, so your logs look fine. But the image itself is useless.

For years the standard fix was puppeteer-extra-plugin-stealth. It's a Puppeteer plugin that hides some of the signals headless browsers give off, like the navigator.webdriver flag or empty plugin lists. It helped for a while. But Cloudflare, DataDome, and Akamai keep changing what they check, and the plugin stopped keeping up. It was basically abandoned in early 2025, with hundreds of open issues on GitHub from developers hitting the same wall: the plugin worked, then it didn't, and no patch lasts more than a few weeks.

ScreenshotRun's stealth parameter works differently. You add one boolean to your API call, and the renderer takes care of the rest: it randomizes the browser fingerprint, strips out automation signals, and watches for Cloudflare challenge pages so it can wait them out before taking the screenshot. When detection methods change, we update things on our side. You don't need to touch your code.

Why screenshots come back blank on protected sites

Bot protection doesn't rely on a single check. It runs several tests at once, scores the browser on how many it fails, and decides whether to show the real page or block the request. If the score is bad enough, you get a challenge page, a CAPTCHA, or a 403 error. Your screenshot captures that instead of the actual site.

The simplest check is navigator.webdriver. Puppeteer and Playwright both set this to true automatically — a flag that literally says "this browser is controlled by automation." Most stealth tools patch this first, and it's easy to do. But modern detection goes further. There's the window.chrome object, which doesn't exist in headless mode. There's the browser's plugin list, which is empty in headless Chrome but has three entries in a normal install. And there's WebGL, the part of the browser that talks to the graphics card. Headless Chrome reports its GPU as "Google SwiftShader" (a software fallback), while a real Chrome on a MacBook would say something like "Intel Iris OpenGL Engine" or an NVIDIA card. That mismatch alone tells the detection system this isn't a real browser.

Canvas fingerprinting goes even deeper. The detection script draws shapes and text on a hidden canvas, reads back the pixel data, and turns it into a hash. Here's the thing: the same drawing comes out slightly different depending on the operating system and its fonts. Chrome on Linux renders Helvetica as Liberation Sans, so the hash is different from Chrome on macOS. But headless browser instances all run on the same Linux server, so they all produce the same hash. Detection systems spot that pattern instantly.

Cloudflare's Turnstile runs all of these checks in a fraction of a second. If the browser fails enough of them, you see the "Checking your browser..." spinner for 3-5 seconds. If your screenshot fires before that clears, you capture the spinner instead of the page. And if the score is very low, the site blocks the request entirely.

How ScreenshotRun's stealth mode works

The renderer has two layers of evasion. The first one runs on every screenshot, whether you set stealth or not. The second kicks in when you add stealth=true.

Base layer (always active)

Every capture gets a set of patches before the page loads. These cover the signals that trip up basic bot detection:

  • navigator.webdriver set to false
  • Chrome plugins array filled with three real entries (Chrome PDF Plugin, Chrome PDF Viewer, Native Client)
  • navigator.languages returns ['en-US', 'en'] instead of an empty array
  • window.chrome object present with runtime, loadTimes, and csi methods
  • Permissions API returns "prompt" for notifications (headless normally returns "denied")
  • WebGL vendor shows "Intel Inc." and "Intel Iris OpenGL Engine" instead of SwiftShader
  • navigator.hardwareConcurrency reports 8 cores, deviceMemory reports 8 GB
  • document.visibilityState forced to "visible" (headless says "hidden," which breaks some sites)

For most websites without aggressive bot protection, these patches do the job. You don't need stealth=true for a regular e-commerce page or a news site with a simple cookie banner.

Full stealth layer (stealth=true)

When you set stealth=true, the renderer adds randomized fingerprinting on top of everything above. Each request gets a different browser profile, so detection systems can't link captures back to the same source. The User-Agent rotates from a pool of recent Chrome versions (135, 136, 137) on Windows and macOS, mobile requests get recent Safari on iOS, and the navigator.platform value stays in sync with whatever UA gets picked — Win32 for Windows agents, MacIntel for Mac. A mismatched platform is an instant red flag. I've seen DataDome catch exactly this on sites where every other check passed.

WebGL renderer strings rotate through real GPU names: Intel Iris, Intel UHD 630, NVIDIA GTX 1650, RTX 3060, AMD RX 580, and a few others. For canvas fingerprinting, the renderer injects a near-invisible pixel change (rgba 0,0,1 at 0.3% opacity) before any canvas export — same page, different hash every time. Web Audio fingerprinting gets the same treatment with tiny frequency shifts on oscillator output. HTTP headers matter just as much as JavaScript properties: Sec-CH-UA, Sec-CH-UA-Platform, and Sec-CH-UA-Mobile all match the randomized profile. Cloudflare cross-checks these headers against the User-Agent string, and if the UA says Chrome 137 but Sec-CH-UA says Chrome 120, the request looks fake. Our profiles are pre-built so every header stays consistent with the chosen UA.

After the page loads, the renderer checks for Cloudflare challenge pages — text like "Checking your browser" or "Verify you are human," plus Cloudflare-specific DOM selectors. If it finds a challenge, it waits up to 15 seconds for it to clear before taking the screenshot. Most Turnstile challenges resolve in 3-5 seconds.

Capturing a Cloudflare-protected page

Add stealth=true to your request. That's the only change you need.

curl "https://api.screenshotrun.com/v1/screenshots/capture?url=https://protected-site.com&stealth=true&delay=2&response_type=json" \
  -H "Authorization: Bearer YOUR_API_KEY"

The delay=2 gives the page two extra seconds after it loads. The renderer already waits for Cloudflare challenges automatically, but some pages load more content after the challenge clears, so a short delay helps make sure everything is on screen before the capture.

Node.js example

const response = await fetch(
  'https://api.screenshotrun.com/v1/screenshots/capture?' + new URLSearchParams({
    url: 'https://protected-site.com',
    stealth: 'true',
    format: 'png',
    full_page: 'true',
    response_type: 'json',
  }),
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const data = await response.json();
console.log(data.data.screenshot_url);

The full Node.js integration guide covers error handling and batch captures.

Python example

import requests

result = requests.get(
    "https://api.screenshotrun.com/v1/screenshots/capture",
    params={
        "url": "https://protected-site.com",
        "stealth": "true",
        "format": "png",
        "full_page": "true",
        "response_type": "json",
    },
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)

print(result.json()["data"]["screenshot_url"])

The full Python integration guide covers async workflows and batch processing.

When you actually need stealth

Most websites don't need it. A company blog, public docs, a government page — these render fine without stealth. It matters when the target site actively tries to detect and block automated browsers.

About half of protected websites use Cloudflare, so if you're keeping an eye on a competitor's pricing page, product catalog, or landing pages, there's roughly a coin-flip chance the site has bot detection. Without stealth, your screenshot shows the "Checking your browser" screen. With stealth=true, the renderer gets past the challenge and captures the actual page. Same story with e-commerce price monitoring — marketplaces and big retailers use DataDome, Akamai Bot Manager, or Cloudflare Enterprise specifically to block price scrapers. Screenshot-based monitoring gets around DOM scraping because you're capturing the visual output, not parsing the HTML. But you still need to get past the bot check first, and stealth mode handles that.

Compliance teams run into this problem from a different angle. Financial services firms, healthcare companies, and legal teams need screenshots as proof of what a third-party website showed at a specific time. If that site uses bot protection, the compliance pipeline breaks without stealth. You can pair stealth=true with certified screenshots for tamper-proof evidence — SHA-256 hash, capture timestamp, and the actual page content instead of a Cloudflare challenge screen. Some teams also hit this with their own staging environments: they put staging behind Cloudflare for security, and then their CI pipeline captures challenge pages instead of the app. Adding stealth=true to the API call fixes it without changing anything about the staging setup.

News sites and government portals are starting to use bot protection more and more. Journalists, researchers, and archival services need screenshots of articles and public documents before they change or get taken down. Without stealth, you're archiving a "Please verify you're human" page instead of the actual content.

Combining stealth with other parameters

Stealth mode works well alongside parameters that solve different rendering problems. If a site is both geo-restricted and bot-protected, add a geolocation and proxy — stealth handles the bot detection while the proxy routes through a country-specific IP. Without stealth, the proxy alone often isn't enough because Cloudflare checks browser fingerprints before it even looks at IP location. For pages that load content after the challenge clears, use wait_for_selector to hold the capture until a specific element shows up. The renderer already waits for the Cloudflare challenge, but the page's own lazy-loaded content might need that extra wait.

Cookie blocking and dark mode both work independently of stealth: block cookies removes the consent banner, dark mode gets you the dark theme, and neither interferes with the detection bypass. You can use stealth=true with full-page screenshots for the entire scrollable content of a protected page, in any output format — PNG, JPEG, WebP, PDF. And for monitoring workflows that run stealth captures on a schedule, use webhooks instead of polling. Stealth captures take a few extra seconds because of challenge resolution, so async delivery keeps your pipeline from waiting around.

The stealth parameter

ParameterTypeDefaultPlanDescription
stealthbooleanfalsePro+Turns on fingerprint randomization and bot detection evasion. Randomizes browser profile, patches automation signals, and waits for Cloudflare challenge resolution.

Stealth mode is available on Pro ($29/mo), Growth ($79/mo), and Business ($119/mo) plans. The free and Starter plans include the base-layer patches (navigator.webdriver, plugins, WebGL vendor) but not the fingerprint randomization or Cloudflare challenge detection.

Limits of browser-level evasion

Bot detection keeps evolving. Anyone claiming 100% bypass rates is selling you something. Most screenshot APIs fail on a large share of aggressively protected targets. We keep updating our evasion, but some detection layers are harder to get past than others.

TLS fingerprinting (JA3/JA4 hashes) works at the network protocol level, below the browser. It compares the connection's cryptographic handshake against known browser patterns, and no amount of JavaScript patching changes what the TLS stack reveals. Most bot protection systems don't weigh these signals heavily, but the premium tiers of DataDome and Akamai do look at them. Behavioral analysis is another blind spot — some detection systems track mouse movements, scroll speed, and typing patterns, but a screenshot API sends one request and captures the result. There's no mouse to move because there's no interactive session. Sites that require behavioral signals (mostly financial services) will still block automated captures, though this is rare.

IP reputation matters too. If your captures come from a datacenter IP that's been flagged for automated traffic, fingerprint randomization alone might not be enough. Combining stealth with a residential proxy via the proxy parameter is usually the next step — residential IPs pass geo and reputation checks on most sites, while datacenter IPs get blocked much more often by aggressive anti-bot systems. Stealth also adds a few seconds to capture time: average goes from about 2-3 seconds to 4-7 seconds on a protected page because of the profile setup and potential challenge wait. No reason to pay that extra time when you don't need it, so leave stealth off for unprotected targets.

Sites with interactive CAPTCHAs — "click all the traffic lights," "slide the puzzle piece" — require human interaction by design. Stealth can't solve those. It handles passive challenges like Cloudflare's JavaScript verification, but interactive puzzles are built to stop automation. If you keep seeing interactive CAPTCHAs on a target, the site has flagged automated traffic at a level that stealth alone won't get past.

How to tell it's working

Without stealth, a Cloudflare-protected site gives you one of three things: the "Checking your browser" spinner, a "Verify you are human" page, or a blank white page where the challenge ran but didn't resolve before the capture timed out. With stealth=true, you get the real page. The easiest way to confirm: your screenshots stop showing Cloudflare's loading spinner. If a screenshot still comes back with a challenge page after enabling stealth, the site probably uses detection beyond browser fingerprinting — IP reputation, behavioral scoring, or interactive CAPTCHAs. Try adding a residential proxy as the next step.

Start with one protected URL you care about. Send two requests: one without stealth, one with stealth=true. Compare the images. If the first shows a challenge and the second shows the real site, you're good to go.

\n\n

Screenshots of real pages, not challenge screens. Add stealth=true and capture protected sites.

Get your free API key

Frequently asked questions

Add stealth=true to your ScreenshotRun API request. The renderer patches browser fingerprints, removes automation signals, and waits for Cloudflare's JavaScript challenge to clear before capturing. If the site also blocks by IP, combine stealth with the proxy parameter using a residential IP.

Headless browsers like Puppeteer and Playwright have detectable fingerprints: navigator.webdriver set to true, missing Chrome plugins, SwiftShader as the WebGL renderer. Bot detection scores these signals and shows a CAPTCHA or challenge page instead of the real content. A stealth screenshot API patches these signals so the browser looks like a normal user session.

Stealth mode patches the JavaScript properties and browser characteristics that bot detection checks. This includes navigator.webdriver, the plugins array, WebGL vendor strings, canvas fingerprints, and Client Hints headers. The goal is to make the automated browser look the same as a real person's Chrome session.

The puppeteer-extra-plugin-stealth was basically abandoned in early 2025 and has hundreds of unresolved issues on GitHub. Cloudflare and DataDome updated their detection to catch the specific patches it uses. ScreenshotRun's stealth parameter is a maintained alternative that gets updated as detection methods change.

ScreenshotRun's stealth mode is built for this. The renderer detects Cloudflare challenge pages, waits for the JavaScript verification to finish, and captures the real page. It also handles DataDome and Akamai Bot Manager on most protected targets.