Tutorials

Captcha solving with Playwright

Keep your Playwright flow and offload the captcha: read the reCAPTCHA v3 site key and action, get a token from CapBypass over HTTP, make grecaptcha.execute return it, and submit.

Captcha solving with Playwright

Playwright drives a real browser, so people expect captchas to just work - but reCAPTCHA v3 still scores the session and lets the site reject automated traffic. This post keeps your Playwright flow and offloads only the token: read the site key and action from the page, get a token from CapBypass over HTTP, hand it to the page's own grecaptcha.execute call, and submit.


why playwright still hits captchas

A real browser clears the "can it run JavaScript" bar, but reCAPTCHA v3 goes further - it scores behaviour and device signals. Automated Playwright sessions often score low enough to be silently rejected. You still end up needing a token the site will accept.

Rather than fight the in-page scoring, solve it out of band and make the page use that token when it asks for one.


detecting the challenge on the page

reCAPTCHA v3 has no widget to click. The page loads api.js?render=<siteKey> and calls grecaptcha.execute(siteKey, {action}) when the form is submitted. Read both from the page Playwright already has open:

import re

async def get_recaptcha_params(page):
    el = await page.query_selector('script[src*="render="]')
    if not el:
        return None
    key = re.search(r"render=([\w-]+)", await el.get_attribute("src")).group(1)
    m = re.search(r"action:\s*['\"]([^'\"]+)", await page.content())
    return key, (m.group(1) if m else "homepage")

If a key comes back, the page is gated and you solve before submitting. If the action isn't in the HTML, it is in a bundled script: see reCAPTCHA action mismatch for how to log it.


solving via the api

Your Playwright script calls the CapBypass HTTP API for the token - no second browser, just a request. Use the Python SDK:

import os
from capbypass import CapBypass

solver = CapBypass(api_key=os.environ["CAPBYPASS_API_KEY"])

def solve(url: str, key: str, action: str) -> str:
    solution = solver.solve({
        "type": "ReCaptchaV3TaskProxyLess",
        "websiteURL": url,
        "websiteKey": key,
        "pageAction": action,
    })
    return solution["gRecaptchaResponse"]

solve() is a blocking call. In a busy async script, run it with await asyncio.to_thread(solve, url, key, action) so the event loop keeps going.


handing the token to the page

The site's own submit handler calls grecaptcha.execute(...) and puts whatever it resolves to into the request. Replace execute with a function that resolves to your solved token, then submit as a user would:

async def pass_recaptcha(page, url):
    key, action = await get_recaptcha_params(page)
    token = solve(url, key, action)

    await page.wait_for_function("window.grecaptcha && window.grecaptcha.execute")
    await page.evaluate(
        """(t) => {
            const fake = () => Promise.resolve(t);
            grecaptcha.execute = fake;
            if (grecaptcha.enterprise) grecaptcha.enterprise.execute = fake;
        }""",
        token,
    )

    await page.click("button[type=submit]")

The page code is untouched: grecaptcha.ready() still fires and the submit handler still builds its own request. Only the token source changes.


Playwright reCAPTCHA v3 flow: read the render site key and execute action from the page, solve ReCaptchaV3TaskProxyLess over HTTP, override grecaptcha.execute to resolve to the solved token, then click submit so the page sends it

enterprise keys and execute on page load

Same shape, two variations:

  • Enterprise: the page loads enterprise.js?render=<siteKey> and calls grecaptcha.enterprise.execute. Detect it with script[src*="enterprise.js"] and solve with ReCaptchaV3EnterpriseTaskProxyLess. The override above already patches grecaptcha.enterprise.execute. More in the reCAPTCHA Enterprise solver guide.
  • Execute on page load: some pages request the token as soon as the script is ready, before you can patch it. Solve first (the key and action don't change between visits), then install the override before navigation so it is in place when the page asks:
await page.add_init_script(f"""
  const fake = () => Promise.resolve({json.dumps(token)});
  const lock = (o) => Object.defineProperty(o, "execute", {{
    configurable: true, get: () => fake, set: () => {{}},
  }});
  let g;
  Object.defineProperty(window, "grecaptcha", {{
    configurable: true,
    get: () => g,
    set: (v) => {{
      g = v; lock(g);
      let e = g.enterprise;
      Object.defineProperty(g, "enterprise", {{
        configurable: true, get: () => e, set: (x) => {{ e = x; lock(e); }},
      }});
      if (e) lock(e);
    }},
  }});
""")
await page.goto(url)

Add import json at the top for that one. The script traps the assignment of window.grecaptcha, so execute stays yours even when Google's script defines it later. We checked both variants in headless Chrome against Google's real api.js and enterprise.js: the page's own execute call resolved to the injected token. See the reCAPTCHA v3 docs for the full task schema.


Bonus: +5% credits on every top-up

New to CapBypass? Apply code WELCOME_2026 at checkout for an extra 5% in credits on every top-up, with no minimum and no expiry. Redeem it on the top-up page and put it toward your first solves.


things that go wrong

  • Override set but the old token is sent. The page called execute before your patch. Use the add_init_script version above.
  • Token accepted by Google, rejected by the site. The action doesn't match. Send exactly what the page passes to execute, case included.
  • Low v3 score even from a browser. Playwright sessions can score low; solving through the API takes the score off your browser session. See why your reCAPTCHA v3 score is low.
  • Stale token. Solve right before submitting; tokens expire in about 2 minutes and are single-use.

faq

Does CapBypass run the browser for me? No - you run Playwright. CapBypass solves the captcha over HTTP and returns a token; you hand it to your own page. It is a plain API call from inside your script.

Where does the token go in Playwright? Nowhere you write by hand. Override grecaptcha.execute (and grecaptcha.enterprise.execute) so it resolves to the solved token, and the page's submit handler puts it in the request itself.

Why is my session still blocked with a valid token? On reCAPTCHA v3 a valid token can still be rejected if the action differs from what the site expects. Match the pageAction to the page's execute call.

Playwright or just the HTTP API? If the site works without a browser, the plain HTTP approach is cheaper and faster: see add captcha solving to a Python scraper. Use Playwright only when the page genuinely needs a rendered session for the rest of the flow.

Ready to start solving CAPTCHAs?

Get started with CapBypass in minutes. No credit card required.

Written by capbypass-bob for the CapBypass engineering team, who build and operate our CAPTCHA and WAF solving infrastructure.
Share:

Related Articles