> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://platform.bctrl.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://platform.bctrl.ai/_mcp/server.

# Solve CAPTCHAs Mid-Automation

> Hit a CAPTCHA with Playwright, clear it with a hosted invocation, and continue the same session.

Your code drives the browser over CDP. When a challenge blocks the flow, hand the same browser to the hosted [CAPTCHA solver](/sdk/captcha) - it runs inside the runtime, on the page your automation is already on - then continue where you left off.

The solver handles reCAPTCHA, Cloudflare Turnstile, hCaptcha, GeeTest, Arkose, DataDome, Amazon WAF, and more, with an AI vision fallback for challenges it doesn't recognize. You never tell it which type is on the page; it detects that itself.

```ts TypeScript
import { Bctrl } from "@bctrl/sdk";
import { chromium } from "playwright";

const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });

const runtime = await bctrl.runtimes.create({
  type: "browser",
  name: "captcha-recipe",
  config: { stealth: "high" },
});
const { connectUrl } = await bctrl.runtimes.start(runtime.id);

const browser = await chromium.connectOverCDP(connectUrl);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = context.pages()[0] ?? (await context.newPage());

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

// Playwright pauses; the solver clears the challenge on the same page.
const solve = await bctrl.runtimes.invocations.createAndWait(runtime.id, {
  action: "solveCaptcha",
  target: "active",
  timeoutMs: 120_000,
});

if (solve.status === "succeeded") {
  await page.click("#recaptcha-demo-submit"); // continue the flow
} else if (solve.error?.code === "invocation.captcha_not_found") {
  // Nothing was blocking the page - carry on.
} else if (solve.error?.code === "invocation.captcha_solve_failed") {
  const retryable = solve.error.details?.retryable === true;
  // Retry the invocation, or hand the page to a human via live view.
}

await browser.close();
await bctrl.runtimes.stop(runtime.id);
```

```python Python
from bctrl import Bctrl
from playwright.sync_api import sync_playwright

bctrl = Bctrl()

with bctrl.runtimes.started_browser(
    name="captcha-recipe", config={"stealth": "high"}
) as rt:
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(rt.connect_url)
        context = browser.contexts[0]
        page = context.pages[0] if context.pages else context.new_page()

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

        # Playwright pauses; the solver clears the challenge on the same page.
        solve = bctrl.runtimes.invocations.create_and_wait(
            rt.runtime_id,
            action="solveCaptcha",
            target="active",
            timeoutMs=120_000,
        )

        error_code = (solve.get("error") or {}).get("code")
        if solve["status"] == "succeeded":
            page.click("#recaptcha-demo-submit")  # continue the flow
        elif error_code == "invocation.captcha_not_found":
            pass  # nothing was blocking the page - carry on
        elif error_code == "invocation.captcha_solve_failed":
            # check solve["error"]["details"]["retryable"], retry or
            # hand the page to a human via live view
            ...

        browser.close()
```

> 🎬 **Content TODO:** record a short GIF/video of a solve happening in the live view (the reCAPTCHA demo page or a GeeTest slider being dragged is the most impressive) and embed it here.

Use one controller at a time: stop issuing CDP commands while the solve runs, the same way you would for any [hosted invocation](/sdk/invocations). The two failure codes are deliberate - `captcha_not_found` means your flow can proceed, `captcha_solve_failed` means it can't, and `details.retryable` tells you whether trying again is worth it.

Every solve also lands on the run's timeline as `captcha.solve.started` / `succeeded` / `failed` [events](/sdk/events), so you can audit how often challenges appear in production.

## Fewer challenges in the first place

Most challenges never fire when the runtime runs with `stealth` and a residential [proxy](/sdk/proxies). Treat the solver as the fallback, not the strategy - see [Runtime configuration](/sdk/runtime-config).

## Next

* [CAPTCHA](/sdk/captcha) - the full solver surface, including agent toolsets
* [Live View](/cookbook/embed-live-view) - hand a stuck page to a human instead
* [Events & Activity](/sdk/events) - track solves on the run timeline