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
Connect Through the Stealth Route
Prevention beats solving. The
/stealthroute 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_HEREThat's the whole change: add
/stealthto the path you already connect to. There are also/chromium/stealthand/chrome/stealthvariants covered in stealth routes.noteStealth 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.
Turn On Automatic Solving
Add
solveCaptchas=trueto 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=300000Raise
timeoutwhile 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.captchaAutoSolvedCDP event only where the next action depends on the solve, such as clicking Submit:- Puppeteer
- Playwright
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();
}import { chromium } from "playwright-core";
const TOKEN = "YOUR_API_TOKEN_HERE";
const browser = await chromium.connectOverCDP(
`wss://production-sfo.browserless.io/stealth?token=${TOKEN}&solveCaptchas=true&timeout=300000`
);
try {
// Reuse the default context and page, or Browserless CDP events won't reach you.
const context = browser.contexts()[0];
const page = context.pages()[0];
const cdp = await context.newCDPSession(page);
// 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: "networkidle",
});
// 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 }tipBoth examples race the event against a
setTimeoutfor a reason: on a page that turns out to have no CAPTCHA, nothing ever fires and a bareawaithangs for the life of the session.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=300000Match
proxyCountryto 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.
- TypeScript
- Python
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();
}
import bap.sync_api as bap
TOKEN = "YOUR_API_TOKEN_HERE"
# The stealth endpoint, so fewer challenges appear in the first place.
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
token=TOKEN,
) as browser:
with browser.page() as page:
# Residential exit IP, matched to the audience the site expects.
page.proxy(country="US", sticky=True, type=["document"])
page.goto("https://www.google.com/recaptcha/api2/demo")
# timeout bounds the wait for a CAPTCHA to appear, not the solve itself.
result = page.solve(timeout=30000)
if result["solved"]:
page.click("#recaptcha-demo-submit")
elif result["found"] is False:
print("no captcha appeared")
else:
print("solve failed:", result["error"])
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.