Tutorials

Python Playwright + CaptchaAI Complete Integration Guide

Playwright offers faster execution, better stealth-configuredion defaults, and native async support compared to Selenium. This guide covers the full integration with CaptchaAI to solve reCAPTCHA, Turnstile, and image CAPTCHAs in Playwright automation scripts.


Prerequisites

pip install playwright aiohttp
playwright install chromium

Async CaptchaAI solver

import aiohttp
import asyncio

API_KEY = "YOUR_API_KEY"


async def solve_captcha(method, **params):
    """Async CaptchaAI solver for Playwright workflows."""
    async with aiohttp.ClientSession() as session:
        # Submit task
        submit_data = {
            "key": API_KEY,
            "method": method,
            "json": 1,
            **params,
        }
        async with session.post("https://ocr.captchaai.com/in.php", data=submit_data) as resp:
            data = await resp.json(content_type=None)
            if data.get("status") != 1:
                raise Exception(f"Submit error: {data.get('request')}")
            task_id = data["request"]

        # Poll for result
        for _ in range(30):
            await asyncio.sleep(5)
            async with session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1,
            }) as resp:
                result = await resp.json(content_type=None)
                if result.get("status") == 1:
                    return result["request"]
                if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
                    raise Exception("CAPTCHA unsolvable")

        raise TimeoutError("Solve timed out")

Stealth-configuredion Playwright browser

from playwright.async_api import async_playwright


async def create_browser():
    """Launch Playwright browser with stealth-configuredion settings."""
    pw = await async_playwright().start()
    browser = await pw.chromium.launch(
        headless=False,
        args=[
            "--disable-blink-features=AutomationControlled",
        ],
    )
    context = await browser.new_context(
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                   "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        viewport={"width": 1920, "height": 1080},
        locale="en-US",
    )

    # Remove Playwright detection signals
    await context.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
        delete navigator.__proto__.webdriver;
    """)

    page = await context.new_page()
    return pw, browser, context, page

reCAPTCHA v2 with Playwright

import re


async def solve_recaptcha_v2_playwright(page, url):
    """Complete reCAPTCHA v2 solve in Playwright."""
    await page.goto(url, wait_until="networkidle")

    # Extract sitekey from the page
    content = await page.content()
    match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', content)
    if not match:
        raise ValueError("reCAPTCHA sitekey not found")

    sitekey = match.group(1)
    print(f"Sitekey: {sitekey}")

    # Solve via CaptchaAI
    token = await solve_captcha(
        "userrecaptcha",
        googlekey=sitekey,
        pageurl=url,
    )
    print(f"Token: {token[:50]}...")

    # Inject token
    await page.evaluate(f"""() => {{
        document.getElementById('g-recaptcha-response').value = '{token}';
        document.getElementById('g-recaptcha-response').style.display = 'block';
    }}""")

    # Trigger callback if available
    await page.evaluate(f"""() => {{
        if (typeof ___grecaptcha_cfg !== 'undefined') {{
            var clients = ___grecaptcha_cfg.clients;
            for (var key in clients) {{
                var client = clients[key];
                try {{
                    Object.keys(client).forEach(function(k) {{
                        if (client[k] && client[k].callback) {{
                            client[k].callback('{token}');
                        }}
                    }});
                }} catch(e) {{}}
            }}
        }}
    }}""")

    # Submit form
    await page.click("button[type='submit'], input[type='submit']")
    await page.wait_for_load_state("networkidle")

    return token

Cloudflare Turnstile with Playwright

async def solve_turnstile_playwright(page, url):
    """Complete Turnstile solve in Playwright."""
    await page.goto(url, wait_until="networkidle")

    content = await page.content()

    # Extract sitekey
    match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', content)
    if not match:
        match = re.search(r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", content)
    if not match:
        raise ValueError("Turnstile sitekey not found")

    sitekey = match.group(1)
    print(f"Turnstile sitekey: {sitekey}")

    # Solve via CaptchaAI
    token = await solve_captcha(
        "turnstile",
        sitekey=sitekey,
        pageurl=url,
    )

    # Inject token into hidden inputs
    await page.evaluate(f"""() => {{
        document.querySelectorAll('[name="cf-turnstile-response"]')
            .forEach(el => el.value = '{token}');
    }}""")

    # Submit
    await page.click("button[type='submit'], input[type='submit']")
    await page.wait_for_load_state("networkidle")

    return token

Image CAPTCHA with Playwright

async def solve_image_captcha_playwright(page, captcha_selector):
    """Solve image CAPTCHA visible on the page."""
    captcha_element = page.locator(captcha_selector)

    # Screenshot the CAPTCHA image
    img_bytes = await captcha_element.screenshot()
    import base64
    img_base64 = base64.b64encode(img_bytes).decode()

    # Solve via CaptchaAI
    answer = await solve_captcha("base64", body=img_base64)
    print(f"Answer: {answer}")

    # Type the answer
    captcha_input = page.locator("input[name='captcha'], input[name='code'], input.captcha-input")
    await captcha_input.fill(answer)

    return answer

Intercepting network requests

Playwright excels at request interception. Use it to extract CAPTCHA parameters from API calls:

async def intercept_captcha_params(page, url):
    """Intercept network requests to find CAPTCHA parameters."""
    captcha_params = {}

    async def handle_request(route, request):
        if "recaptcha" in request.url or "turnstile" in request.url:
            from urllib.parse import urlparse, parse_qs
            parsed = urlparse(request.url)
            params = parse_qs(parsed.query)
            captcha_params.update(params)
            print(f"Intercepted: {request.url}")
        await route.continue_()

    await page.route("**/*", handle_request)
    await page.goto(url, wait_until="networkidle")
    await page.unroute("**/*")

    return captcha_params

Complete automation class

import re
import asyncio
import aiohttp
import base64
from playwright.async_api import async_playwright


API_KEY = "YOUR_API_KEY"


class PlaywrightCaptchaSolver:
    """Complete Playwright + CaptchaAI automation class."""

    def __init__(self, api_key, headless=False):
        self.api_key = api_key
        self.headless = headless
        self.pw = None
        self.browser = None
        self.context = None
        self.page = None

    async def start(self):
        """Initialize the browser."""
        self.pw = await async_playwright().start()
        self.browser = await self.pw.chromium.launch(
            headless=self.headless,
            args=["--disable-blink-features=AutomationControlled"],
        )
        self.context = await self.browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                       "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            viewport={"width": 1920, "height": 1080},
        )
        await self.context.add_init_script(
            "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
        )
        self.page = await self.context.new_page()

    async def stop(self):
        """Close the browser."""
        if self.browser:
            await self.browser.close()
        if self.pw:
            await self.pw.stop()

    async def navigate(self, url):
        """Navigate and wait for page to load."""
        await self.page.goto(url, wait_until="networkidle")

    async def detect_captcha(self):
        """Detect which CAPTCHA type is present."""
        content = await self.page.content()

        if re.search(r'data-sitekey=["\'][A-Za-z0-9_-]{40}["\']', content):
            if "recaptcha" in content.lower():
                return "recaptcha_v2"

        if "cf-turnstile" in content or "challenges.cloudflare.com/turnstile" in content:
            return "turnstile"

        if re.search(r"render=[A-Za-z0-9_-]{40}", content):
            return "recaptcha_v3"

        img_count = await self.page.locator(
            "img.captcha, img[alt*='captcha'], img[src*='captcha']"
        ).count()
        if img_count > 0:
            return "image"

        return None

    async def solve_and_submit(self, url, form_data=None):
        """Full workflow: navigate, detect, solve, fill, submit."""
        await self.navigate(url)
        captcha_type = await self.detect_captcha()

        if captcha_type:
            print(f"Detected: {captcha_type}")
            await self._solve(captcha_type)

        if form_data:
            for name, value in form_data.items():
                try:
                    await self.page.fill(f"[name='{name}']", value)
                except Exception:
                    pass

        await self.page.click("button[type='submit'], input[type='submit']")
        await self.page.wait_for_load_state("networkidle")
        return self.page.url

    async def _solve(self, captcha_type):
        content = await self.page.content()
        url = self.page.url

        if captcha_type == "recaptcha_v2":
            match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', content)
            token = await self._api_solve("userrecaptcha", googlekey=match.group(1), pageurl=url)
            await self.page.evaluate(f"""() => {{
                document.getElementById('g-recaptcha-response').value = '{token}';
            }}""")

        elif captcha_type == "turnstile":
            match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', content)
            token = await self._api_solve("turnstile", sitekey=match.group(1), pageurl=url)
            await self.page.evaluate(f"""() => {{
                document.querySelectorAll('[name="cf-turnstile-response"]')
                    .forEach(el => el.value = '{token}');
            }}""")

        elif captcha_type == "image":
            img = self.page.locator("img.captcha, img[alt*='captcha'], img[src*='captcha']").first
            img_bytes = await img.screenshot()
            answer = await self._api_solve("base64", body=base64.b64encode(img_bytes).decode())
            await self.page.fill("input[name='captcha'], input[name='code']", answer)

    async def _api_solve(self, method, **params):
        async with aiohttp.ClientSession() as session:
            async with session.post("https://ocr.captchaai.com/in.php", data={
                "key": self.api_key, "method": method, "json": 1, **params,
            }) as resp:
                data = await resp.json(content_type=None)
                if data.get("status") != 1:
                    raise Exception(f"Submit error: {data.get('request')}")
                task_id = data["request"]

            for _ in range(30):
                await asyncio.sleep(5)
                async with session.get("https://ocr.captchaai.com/res.php", params={
                    "key": self.api_key, "action": "get", "id": task_id, "json": 1,
                }) as resp:
                    result = await resp.json(content_type=None)
                    if result.get("status") == 1:
                        return result["request"]
            raise TimeoutError("Solve timed out")


# Usage
async def main():
    solver = PlaywrightCaptchaSolver(API_KEY)
    await solver.start()
    try:
        result = await solver.solve_and_submit(
            "https://example.com/login",
            form_data={"email": "user@example.com", "password": "pass123"},
        )
        print(f"Result: {result}")
    finally:
        await solver.stop()


asyncio.run(main())

Playwright vs Selenium for CAPTCHA solving

Feature Playwright Selenium
Async native Yes No (requires threading)
Stealth-configuredion Better defaults Requires more configuration
Speed Faster Slower page loads
Request interception Built-in Requires proxy/extension
Multi-browser Chromium, Firefox, WebKit Chrome, Firefox, Edge, Safari
API style Promise-based, modern Imperative, traditional

Troubleshooting

Symptom Cause Fix
page.evaluate fails Content not loaded Use wait_until="networkidle"
Token injection doesn't work Wrong element selector Inspect with page.content() to find actual element
Playwright detection Missing init script Add webdriver override in add_init_script
Timeout on networkidle Infinite polling scripts Use wait_until="domcontentloaded" instead
Image screenshot is blank Element hidden Scroll into view: await element.scroll_into_view_if_needed()

Frequently asked questions

Should I use Playwright or Selenium for CAPTCHA solving?

Use Playwright for new projects — it has better performance, native async support, and better stealth-configuredion defaults. Use Selenium if you have an existing Selenium codebase.

Can Playwright run in headless mode?

Yes. Set headless=True in launch(). Some sites detect headless mode, so test both configurations. CaptchaAI solves on its own infrastructure, so headless vs headed doesn't affect solve success.

How do I handle pages that load CAPTCHA dynamically?

Use page.wait_for_selector to wait for the CAPTCHA element to appear, or use page.wait_for_function to wait for the CAPTCHA JavaScript to be ready.


Summary

Python Playwright + CaptchaAI delivers a modern async CAPTCHA automation stack. Use PlaywrightCaptchaSolver for the complete detect-solve-submit workflow with native async support, request interception, and strong stealth-configuredion defaults.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.