Developer Guides

How to Use a CAPTCHA Solver with Playwright

Playwright is the modern alternative to Selenium — async-native, faster, and with better network interception APIs. This guide covers the solve-and-inject integration pattern for CAPTCHAs in Playwright, with Python examples and equivalent Node.js snippets.

Prerequisites

Python:

pip install playwright requests
playwright install chromium

Node.js:

npm install playwright axios
npx playwright install chromium

Core Strategy: Network Interception vs. Element Injection

Playwright offers two approaches for CAPTCHA integration:

  1. Element injection (same as Selenium) — solve via API, inject token into the hidden field, submit. Works for all CAPTCHA types.
  2. Network interception — intercept the CAPTCHA initialization request to extract parameters dynamically. Useful when site keys are loaded via XHR (common with GeeTest and some FunCaptcha deployments).

This guide covers both.

Solving reCAPTCHA v2 in Python

import requests
import time
from playwright.sync_api import sync_playwright


def solve_recaptcha_v2(api_key: str, page_url: str, site_key: str) -> str:
    r = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": "userrecaptcha",
        "googlekey": site_key, "pageurl": page_url, "json": 1,
    }, timeout=30)
    r.raise_for_status()
    data = r.json()
    if data.get("status") != 1:
        raise RuntimeError(f"Submit failed: {data}")
    task_id = data["request"]

    time.sleep(5)
    for _ in range(24):
        r = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get", "id": task_id, "json": 1,
        }, timeout=30)
        data = r.json()
        if data.get("status") == 1:
            return data["request"]
        time.sleep(5)
    raise TimeoutError("reCAPTCHA v2 solve timed out")


def submit_recaptcha_form(page_url: str, api_key: str) -> None:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(page_url, wait_until="networkidle")

        # Extract site key
        site_key = page.get_attribute("[data-sitekey]", "data-sitekey")
        if not site_key:
            raise ValueError("Site key not found")

        # Solve
        token = solve_recaptcha_v2(api_key, page_url, site_key)

        # Inject token
        page.evaluate(f"""
            document.querySelectorAll('[name="g-recaptcha-response"]')
                .forEach(el => {{ el.value = '{token}'; }});
        """)

        # Dispatch change event to activate submit button
        page.evaluate("""
            document.querySelectorAll('[name="g-recaptcha-response"]')
                .forEach(el => el.dispatchEvent(new Event('change', {{bubbles: true}})));
        """)

        page.click('button[type="submit"]')
        page.wait_for_load_state("networkidle")
        browser.close()

Solving hCaptcha in Python

def solve_hcaptcha(api_key: str, page_url: str, site_key: str) -> str:
    r = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": "hcaptcha",
        "sitekey": site_key, "pageurl": page_url, "json": 1,
    }, timeout=30)
    r.raise_for_status()
    data = r.json()
    if data.get("status") != 1:
        raise RuntimeError(f"Submit failed: {data}")
    task_id = data["request"]

    time.sleep(7)
    for _ in range(24):
        r = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get", "id": task_id, "json": 1,
        }, timeout=30)
        data = r.json()
        if data.get("status") == 1:
            return data["request"]
        time.sleep(5)
    raise TimeoutError("hCaptcha solve timed out")


def submit_hcaptcha_form(page_url: str, api_key: str) -> None:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(page_url, wait_until="networkidle")

        site_key = page.get_attribute("[data-sitekey]", "data-sitekey")
        token = solve_hcaptcha(api_key, page_url, site_key)

        page.evaluate(f"""
            document.querySelectorAll('[name="h-captcha-response"]')
                .forEach(el => {{ el.value = '{token}'; }});
        """)
        page.click('button[type="submit"]')
        page.wait_for_load_state("networkidle")
        browser.close()

Solving Cloudflare Turnstile in Python

def submit_turnstile_form(page_url: str, api_key: str) -> None:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(page_url, wait_until="networkidle")

        # Turnstile sitekeys start with 0x4
        site_key = page.get_attribute("[data-sitekey]", "data-sitekey")

        # Solve
        r = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": api_key, "method": "turnstile",
            "sitekey": site_key, "pageurl": page_url, "json": 1,
        }, timeout=30)
        task_id = r.json()["request"]
        time.sleep(5)
        for _ in range(24):
            r = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": api_key, "action": "get", "id": task_id, "json": 1,
            }, timeout=30)
            data = r.json()
            if data.get("status") == 1:
                token = data["request"]
                break
            time.sleep(5)

        page.evaluate(f"""
            document.querySelectorAll('[name="cf-turnstile-response"]')
                .forEach(el => {{ el.value = '{token}'; }});
        """)
        page.click('button[type="submit"]')
        page.wait_for_load_state("networkidle")
        browser.close()

Using Network Interception to Extract Parameters

For dynamic parameters (GeeTest challenge, FunCaptcha public key loaded via XHR), use Playwright's route or response listener:

import json

def extract_geetest_params_via_interception(page_url: str) -> dict:
    captured = {}

    def on_response(response):
        if "captcha/register" in response.url or "/gt/register-slide" in response.url:
            try:
                data = response.json()
                if "gt" in data:
                    captured.update(data)
            except Exception:
                pass

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.on("response", on_response)
        page.goto(page_url, wait_until="networkidle")
        browser.close()

    return captured

Async Playwright (Python)

For high-volume pipelines, use the async API to run multiple solves concurrently:

import asyncio
import aiohttp
from playwright.async_api import async_playwright


async def solve_recaptcha_async(session: aiohttp.ClientSession, api_key: str, page_url: str, site_key: str) -> str:
    async with session.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": "userrecaptcha",
        "googlekey": site_key, "pageurl": page_url, "json": 1,
    }) as r:
        data = await r.json()
    task_id = data["request"]

    await asyncio.sleep(5)
    for _ in range(24):
        async with session.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get", "id": task_id, "json": 1,
        }) as r:
            data = await r.json()
        if data.get("status") == 1:
            return data["request"]
        await asyncio.sleep(5)
    raise TimeoutError("Timed out")


async def solve_multiple(api_key: str, tasks: list[dict]) -> list[str]:
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*[
            solve_recaptcha_async(session, api_key, t["url"], t["sitekey"])
            for t in tasks
        ])

Node.js Equivalent (Playwright)

const { chromium } = require('playwright');
const axios = require('axios');

async function submitWithRecaptcha(pageUrl, apiKey) {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(pageUrl, { waitUntil: 'networkidle' });

  const siteKey = await page.getAttribute('[data-sitekey]', 'data-sitekey');

  // Solve via CaptchaAI
  const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: { key: apiKey, method: 'userrecaptcha', googlekey: siteKey, pageurl: pageUrl, json: 1 },
  });
  const taskId = submit.data.request;

  await new Promise(r => setTimeout(r, 5000));
  let token;
  for (let i = 0; i < 24; i++) {
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: apiKey, action: 'get', id: taskId, json: 1 },
    });
    if (poll.data.status === 1) { token = poll.data.request; break; }
    await new Promise(r => setTimeout(r, 5000));
  }

  await page.evaluate((t) => {
    document.querySelectorAll('[name="g-recaptcha-response"]').forEach(el => { el.value = t; });
  }, token);

  await page.click('button[type="submit"]');
  await page.waitForLoadState('networkidle');
  await browser.close();
}

Production Readiness Notes

Use How to Use a CAPTCHA Solver with Playwright as a decision and implementation aid, not just as a one-time reference. The practical test for captcha solver playwright is whether the same approach behaves reliably when traffic is messy: rotating sessions, expired tokens, changing widget parameters, intermittent solver delays, and target pages that refresh without warning. For Automation developer, the safest rollout is to start with a narrow fixture, record every submitted task, and compare the solver response with the browser state that finally submits the form. That makes failures explainable instead of mysterious, especially when a target alternates between visible challenges, invisible checks, and server-side verification.

Evaluation Criteria

A developer guide should become a reusable integration module with typed configuration, bounded polling, structured errors, and a single place for API credentials. For developer integration work, the most useful scorecard combines technical acceptance with operational cost. A low nominal price is not enough if retries double the real cost per accepted token, and a fast median solve time is not enough if p95 latency stalls the queue. Track these criteria before you standardize the workflow:

  • The challenge subtype, sitekey, action, rqdata, blob, captchaId, or page URL used for each task.
  • Median and p95 solve time, separated by provider and target domain.
  • Accepted-token rate on the target page, not just successful API responses.
  • Retry count, timeout count, zero-balance incidents, and invalid-parameter errors.
  • The exact browser, proxy region, and user-agent that submitted the solved token.

Rollout Checklist

Before this guidance moves into a production job, build a small acceptance suite around the pages that matter most. Run it with a fixed browser profile, then repeat with the proxy and concurrency settings you expect in production. Keep the first release conservative: bounded polling, clear timeout handling, and a fallback path when the solver cannot return a usable answer. For developer integration, treat the code as a production pattern: timeouts, retries, logging, secret storage, and test fixtures matter as much as the solve request itself. That checklist keeps the article useful after the first copy-paste, because the integration is judged by end-to-end completion rather than by whether a code sample returned a string.

Monitoring Signals

Healthy CAPTCHA automation is observable. Log the task id, provider, challenge type, target host, queue time, solve time, final submit status, and normalized error code for every attempt. Review those logs in daily batches at first, then move to alerts once the baseline is stable. Sudden drops usually come from target-side changes: a new sitekey, a changed action name, a stricter hostname check, an added managed challenge, or a proxy pool that no longer matches the expected geography. When you can see those shifts quickly, provider switching becomes a controlled decision instead of a late-night rewrite.

Maintenance Cadence

Revisit the setup whenever the target UI changes, when the solver provider changes task names or pricing, or when benchmark data shows a sustained latency or solve-rate shift. Keep one known-good fixture for each CAPTCHA subtype and rerun it after dependency upgrades, browser updates, and proxy changes. If the article is used for vendor selection, repeat the same fixture across at least two providers before renewing a balance or migrating the whole pipeline. That habit keeps captcha solver playwright work aligned with the real target behavior rather than with stale assumptions.

Comments are disabled for this article.

Related Posts

Developer Guides How to Use a CAPTCHA Solver with Selenium
Step-by-step guide to integrating a CAPTCHA solver into Selenium automation.

Step-by-step guide to integrating a CAPTCHA solver into Selenium automation. Covers re CAPTCHA v 2, h Captcha,...

May 04, 2026
hCaptcha How to Solve hCaptcha in Python
Complete Python tutorial for solving h Captcha automatically — covers site key extraction, solver API integration with Captcha AI, token injection using Playwri...

Complete Python tutorial for solving h Captcha automatically — covers site key extraction, solver API integrat...

May 05, 2026
reCAPTCHA How to Solve reCAPTCHA v2 in Python
Complete Python tutorial for solving re CAPTCHA v 2 (checkbox and invisible) automatically — includes site key extraction, solver API integration, token injecti...

Complete Python tutorial for solving re CAPTCHA v 2 (checkbox and invisible) automatically — includes site key...

May 05, 2026