Solving CAPTCHAs with BAP
BAP solves CAPTCHAs inside the running session, so the page that was blocked is the page that continues once the challenge clears. This guide covers solve() for interactive challenges like Cloudflare and reCAPTCHA, and solveImageCaptcha() for image-plus-input forms.
- A Browserless API token from your account dashboard
- The BAP SDK installed and connecting, from the BAP Quickstart
- A plan with CAPTCHA solving, since solves consume units
Solve an interactive challenge
solve() waits for a CAPTCHA to appear, solves it, and resolves with what happened. With no type it auto-detects the challenge, which is the right default because protected sites rotate providers. Pass a type when you know the site and want to skip detection.
- TypeScript
- Python
import Browserless from "@browserless.io/bap-ts";
const TOKEN = "YOUR_API_TOKEN_HERE";
const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
token: TOKEN,
});
try {
const page = await browser.newPage();
await page.goto("https://example.com/protected");
// Auto-detects the challenge type. timeout bounds the wait for the
// CAPTCHA to appear, not the solve itself.
const result = await page.solve({ timeout: 30_000 });
if (result.solved) {
// The session continues where the challenge was.
console.log(await page.title());
} else if (result.found === false) {
// found is nullable: false means no CAPTCHA, null means no value came back.
console.log("no captcha appeared");
} else {
console.error("solve failed:", result.error);
}
} finally {
// A rejected goto() or solve() would otherwise leave the session billing.
await browser.close();
}
import bap.sync_api as bap
TOKEN = "YOUR_API_TOKEN_HERE"
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
token=TOKEN,
) as browser:
with browser.page() as page:
page.goto("https://example.com/protected")
# Auto-detects the challenge type. timeout bounds the wait for the
# CAPTCHA to appear, not the solve itself.
result = page.solve(timeout=30000)
if result["solved"]:
# The session continues where the challenge was.
print(page.title())
elif result["found"] is False:
# found is nullable: False means no CAPTCHA, None means no value came back.
print("no captcha appeared")
else:
print("solve failed:", result["error"])
Response when the solve succeeds:
Protected Page Title
Reading the result
solve() doesn't throw when a challenge can't be cleared. It reports through the response, so branch on the fields:
| Field | Meaning |
|---|---|
found | Whether a CAPTCHA was present at all. false with solved: false means the page wasn't challenged |
solved | Whether the found CAPTCHA was cleared |
token | The solve token, when the provider issues one you need to submit yourself |
time | Total milliseconds to find and solve |
error | What went wrong, with guidance on fixing it |
Supported types
Auto-detection covers the common providers, including Cloudflare, reCAPTCHA v2 and v3, GeeTest, DataDome, and Akamai. When targeting explicitly, type accepts any value of the schema-generated CaptchaType enum. Prefer auto-detect unless a site stacks multiple widgets and you need to pick one.
Waiting behavior
wait defaults to true, so solve() blocks until a CAPTCHA shows up or timeout elapses. That makes the click-then-solve pattern safe: trigger the action that provokes the challenge, then call solve(), and it catches the CAPTCHA whenever it renders. Pass wait: false to check for an already-visible challenge without blocking, which suits polling loops that mix solving with other work.
Solve an image CAPTCHA
Forms that show a distorted image next to a text input need solveImageCaptcha(). You point it at both elements and it fills the input with the solved value.
- TypeScript
- Python
await page.solveImageCaptcha({
captchaSelector: "#captcha-image",
inputSelector: "#captcha-answer",
timeout: 30_000,
});
await page.click("#submit");
page.solve_image_captcha(
captcha_selector="#captcha-image",
input_selector="#captcha-answer",
timeout=30000,
)
page.click("#submit")
Both selectors are required. timeout covers the whole operation here, appearance and solve together, unlike solve() where it only bounds the wait for the challenge to appear.
Avoid CAPTCHAs before solving them
Solving costs time and units, so treat it as the last layer, not the first. Sessions on the stealth endpoint with a residential proxy get challenged far less than plain Chromium, and humanized typing keeps a cleared session from being re-challenged mid-form. The cheapest CAPTCHA is the one that never renders.
FAQ & Troubleshooting
solve() returns found: false but I can see the CAPTCHA
The challenge is probably inside a frame or shadow root that rendered after your call, or it's a provider auto-detection missed. Raise timeout so detection has longer to catch a slow-rendering widget, or pass the provider explicitly with type.
The CAPTCHA was solved but the page still blocks me
Some providers bind the solve to browsing behavior after the challenge. Keep the session on the stealth endpoint, keep the same proxy IP with sticky: true, and interact with default humanization rather than jumping straight to a protected URL.
Do failed solves throw an exception?
No. solve() reports through solved, found, and error on the response, so check those fields. Only transport failures and timeouts raise, as covered in error handling.
What do I do with the token field?
Usually nothing: the solve is applied inside the page. Providers like reCAPTCHA v3 hand back a token your own form submission may need to include, which is when you read it from the response and inject it yourself.