For AI agents: a documentation index is available at /llms.txt
Skip to main content

Bypass CAPTCHAs on a website

Two things get you past a CAPTCHA: not triggering one in the first place, and solving the ones that appear anyway. This page covers both, starting with a one-line change to your connection URL. It picks up from a working connection, so start with Connect Puppeteer or Connect Playwright if you don't have one yet. Writing new automation rather than adapting a script? Skip to the BAP version, which does the same job in one method call.

Steps

  1. Connect Through the Stealth Route

    Prevention beats solving. The /stealth route applies fingerprint mitigations and realistic browser entropy, so many sites never challenge the session at all. Solving a CAPTCHA costs units and takes seconds; never seeing one costs nothing.

    wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE

    That's the whole change: add /stealth to the path you already connect to. There are also /chromium/stealth and /chrome/stealth variants covered in stealth routes.

    note

    Stealth routes alter browser behavior, which occasionally surprises automation that depends on stock Chrome internals. Reach for them when you're being blocked, not as a default connection.

  2. Turn On Automatic Solving

    Add solveCaptchas=true to the same URL. Browserless then watches the whole session and solves challenges as they appear, including ones that load after a later navigation or inside a popup. It handles the common types, reCAPTCHA and Cloudflare Turnstile among them.

    wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&solveCaptchas=true&timeout=300000

    Raise timeout while you're at it. Solving takes seconds to minutes, and the default session timeout can expire mid-solve.

    The solved token is injected into the page for you, so most scripts need nothing else. Wait on the Browserless.captchaAutoSolved CDP event only where the next action depends on the solve, such as clicking Submit:

    import puppeteer from "puppeteer-core";

    const TOKEN = "YOUR_API_TOKEN_HERE";

    const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&solveCaptchas=true&timeout=300000`,
    });

    try {
    const page = await browser.newPage();
    const cdp = await page.createCDPSession();

    // Attach before navigating. The event can fire during goto().
    const captchaSolved = new Promise((resolve) => {
    cdp.on("Browserless.captchaAutoSolved", resolve);
    });

    await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle2",
    });

    // Race a timeout, or a page with no CAPTCHA waits forever.
    const { solved, time } = await Promise.race([
    captchaSolved,
    new Promise((_, reject) =>
    setTimeout(() => reject(new Error("CAPTCHA timeout")), 30000)
    ),
    ]);
    console.log({ solved, time });

    await page.click("#recaptcha-demo-submit");
    } finally {
    await browser.close();
    }

    Output

    { solved: true, time: 21482 }
    tip

    Both examples race the event against a setTimeout for a reason: on a page that turns out to have no CAPTCHA, nothing ever fires and a bare await hangs for the life of the session.

  3. Add a Residential Proxy for Tougher Challenges

    Datacenter IPs are a strong bot signal on their own, which is why some sites, Cloudflare-fronted ones in particular, keep challenging a stealth session. Routing through residential IPs removes that signal:

    wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&solveCaptchas=true&proxy=residential&proxyCountry=us&timeout=300000

    Match proxyCountry to the audience the site expects. A US retailer served through a European exit node draws exactly the scrutiny you're trying to avoid. Residential traffic is billed by bandwidth, so turn it on for the sites that need it rather than globally. See proxies for the full parameter list.

The shorter version with BAP

Everything above is CDP plumbing: attach a listener, race a timeout, read an event. BAP folds that into one method. solve() waits for a challenge, auto-detects its type, solves it, and returns what happened, and the session carries on from the page that was blocked.

import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

// The stealth endpoint, so fewer challenges appear in the first place.
const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
token: TOKEN,
});

try {
const page = await browser.newPage();

// Residential exit IP, matched to the audience the site expects.
await page.proxy({ country: "US", sticky: true, type: ["document"] });

await page.goto("https://www.google.com/recaptcha/api2/demo");

// timeout bounds the wait for a CAPTCHA to appear, not the solve itself.
const result = await page.solve({ timeout: 30_000 });

if (result.solved) {
await page.click("#recaptcha-demo-submit");
} else if (result.found === false) {
console.log("no captcha appeared");
} else {
console.error("solve failed:", result.error);
}
} finally {
await browser.close();
}

Note that solve() doesn't throw when a challenge can't be cleared. It reports through the result, so branch on found and solved rather than wrapping it in a try/catch and assuming success. The BAP CAPTCHA guide covers image CAPTCHAs and the full result shape.

What this costs

Each successful solve costs 10 units. Attempts that don't clear the challenge aren't charged. Residential proxy traffic is billed separately by bandwidth. That pricing is the reason the order above matters: stealth first, solving second, proxies only where they earn their place.

When automatic solving isn't enough

Some flows need a person. Live URLs let you hand a running session to a human who finishes the challenge in a browser window while your script waits, then continues. See hybrid automation.

For control over exactly when a solve happens, skip solveCaptchas=true and drive it yourself with the Browserless.captchaFound event and the Browserless.solveCaptcha command. The CAPTCHA solving reference documents both modes, the full event payloads, and the response fields.

FAQ & Troubleshooting

The script hangs waiting for Browserless.captchaAutoSolved.

There was probably no CAPTCHA to solve, which means no event fires. Race the wait against a timeout, and only await the event where the next step genuinely depends on a solve.

I'm using Playwright and no CDP events arrive.

You're on a fresh context. connectOverCDP gives you a browser that already has a default context open, and Browserless events are bound to it. Use browser.contexts()[0] and context.pages()[0] rather than browser.newPage().

The CAPTCHA solves, but the site still blocks me.

Solving clears one challenge, not the underlying detection. Add a residential proxy, and make sure the session isn't leaking other signals such as a mismatched timezone or locale. Stealth routes and launch parameters cover the knobs worth setting.

Do the REST APIs solve CAPTCHAs too?

/smart-scrape solves challenges that block a page from loading, and /unblock is built for bot-detection bypass. Neither fills in or submits forms, so a CAPTCHA sitting next to a Submit button needs a browser session or BrowserQL.

Next steps

Was this page helpful?