Logo of «2Captcha»To home page
Captcha bypass tutorials

Was this helpful?

How to bypass GeeTest with Playwright

Gregory Fisher
Gregory Fisher

Technical engineer

GeeTest is one of the most popular captchas on the market, especially in Asia and among large e-commerce platforms. It looks like a slider puzzle where you need to drag an image piece to the correct spot, but under the hood, it hides a serious user behavior verification system.

In this guide, we will break down the complete cycle of bypassing GeeTest in Playwright: determine the captcha version (v3 or v4), extract the required parameters, obtain a solution via the 2Captcha API, and correctly inject the response tokens into the form.

Note: GeeTest v3 and v4 require completely different parameters. If you send a v3 challenge to a v4 task, the captcha will fail. Therefore, the first and most crucial step is to correctly identify the version.

What You Will Need

  • Python 3.7 or higher
  • Installed Playwright
  • 2Captcha account and API key
  • Target page URL with GeeTest
  • Optional: proxy, if the site is sensitive to IP addresses

Step 1. Installing Dependencies

Install the required libraries and Playwright browsers:

bash Copy
pip install playwright 2captcha-python
playwright install chromium

Step 2. Browser Initialization and Version Detection

Let us start by opening the page and determining which GeeTest version we are dealing with. For debugging, use visible mode headless=False.

python Copy
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://2captcha.com/demo/geetest")
    
    # Wait for the GeeTest widget to appear
    page.wait_for_selector(".geetest_panel_box")

To understand the version, open DevTools and check the page source code or the Network tab:

  • If you see a challenge next to the gt value, it is v3.
  • If you only see a captcha_id, it is v4.
  • The v4 script is loaded from gcaptcha4.geetest.com/load.

Step 3. Extracting Parameters for GeeTest v3

Version v3 requires two values: gt (a static site key) and challenge (a dynamic, one-time-use token).

python Copy
# Extract static gt
gt = page.evaluate("window.GeeTest_gt || document.querySelector('.geetest_wind').getAttribute('data-gt')")

# Extract dynamic challenge
# Important: challenge is generated every time the captcha opens and is single-use!
challenge = page.evaluate("""
    () => {
        // Look in network requests or global variables
        return window.GeeTest_challenge || 
               document.querySelector('script[data-challenge]')?.getAttribute('data-challenge');
    }
""")

print(f"GT: {gt}")
print(f"Challenge: {challenge}")

Important: the challenge parameter is one-time use. If you copy it once and reuse it, the API will return an expired token error. Before every task submission, you must intercept a fresh challenge from the page's network request to /ajax.php or a similar GeeTest endpoint.

To intercept it, use route or listen to network responses:

python Copy
captured_challenge = None

def handle_response(response):
    global captured_challenge
    if "ajax.php" in response.url and "gt=" in response.url:
        # Extract challenge from the request URL
        import urllib.parse
        params = urllib.parse.parse_qs(urllib.parse.urlparse(response.url).query)
        if "challenge" in params:
            captured_challenge = params["challenge"][0]

page.on("response", handle_response)

# Reload the page or click the widget to trigger the request
page.reload()
page.wait_for_timeout(3000)

print(f"Intercepted fresh challenge: {captured_challenge}")

Step 4. Extracting Parameters for GeeTest v4

With v4, everything is simpler: you only need the static captcha_id, which does not expire.

python Copy
# Extract captcha_id from the load script or DOM
captcha_id = page.evaluate("""
    () => {
        // Look in the v4 script URL
        const scripts = document.querySelectorAll('script[src*="gcaptcha4"]');
        for (const script of scripts) {
            const url = new URL(script.src);
            if (url.searchParams.has('captcha_id')) {
                return url.searchParams.get('captcha_id');
            }
        }
        // Or look in the initGeetest4 call
        return window.captchaId || null;
    }
""")

print(f"Captcha ID: {captcha_id}")

Step 5. Sending the Task to the API and Getting Tokens

We will use the official Python SDK. It automatically creates a task and polls the server until a result is obtained.

For GeeTest v3:

python Copy
from twocaptcha import TwoCaptcha

solver = TwoCaptcha('YOUR_API_KEY')

result = solver.geetest(
    gt=gt,
    challenge=captured_challenge,
    url=page.url
)

print(f"v3 Response: {result}")
# result contains: challenge, validate, seccode

For GeeTest v4:

python Copy
result = solver.geetest_v4(
    captcha_id=captcha_id,
    url=page.url
)

print(f"v4 Response: {result}")
# result contains: captcha_id, lot_number, pass_token, gen_time, captcha_output

Step 6. Injecting the Response into the Form

The response must be injected into the form exactly as GeeTest itself would do after manual solving.

For GeeTest v3, inject three tokens:

python Copy
page.evaluate(f"""
    const form = document.querySelector('form');
    
    // Create hidden fields if they do not exist
    const fields = ['geetest_challenge', 'geetest_validate', 'geetest_seccode'];
    const values = ['{result['challenge']}', '{result['validate']}', '{result['seccode']}'];
    
    fields.forEach((name, i) => {{
        let input = form.querySelector(`input[name="${{name}}"]`);
        if (!input) {{
            input = document.createElement('input');
            input.type = 'hidden';
            input.name = name;
            form.appendChild(input);
        }}
        input.value = values[i];
    }});
    
    // Trigger callback if defined
    if (typeof window.geetestCallback === 'function') {{
        window.geetestCallback('{result['validate']}');
    }}
""")

For GeeTest v4, inject five fields:

python Copy
page.evaluate(f"""
    const form = document.querySelector('form');
    
    const fields = ['lot_number', 'pass_token', 'gen_time', 'captcha_output'];
    const values = [
        '{result['lot_number']}',
        '{result['pass_token']}',
        '{result['gen_time']}',
        '{result['captcha_output']}'
    ];
    
    fields.forEach((name, i) => {{
        let input = form.querySelector(`input[name="${{name}}"]`);
        if (!input) {{
            input = document.createElement('input');
            input.type = 'hidden';
            input.name = name;
            form.appendChild(input);
        }}
        input.value = values[i];
    }});
    
    // Trigger callback for v4
    if (typeof window.captchaCallback === 'function') {{
        window.captchaCallback('{result['pass_token']}');
    }}
""")

Step 7. Submitting the Form

After injecting the tokens, the form should consider itself valid.

python Copy
page.click("button[type='submit']")
page.wait_for_load_state("networkidle")

browser.close()

Troubleshooting Common Issues

Issue Cause and Solution
API returns an expired challenge error For v3, the challenge is one-time use. Intercept a fresh challenge from the network request before every task submission.
Cannot find captcha_id for v4 The script loads dynamically. Wait for the script tag with a src containing gcaptcha4.geetest.com to appear, and extract the captcha_id parameter from the URL.
Form does not submit after token injection The site expects a JavaScript callback to be triggered. Find the function name in the data-callback attribute or in the GeeTest initialization code, and call it via page.evaluate.
Additional behavior verification appears GeeTest analyzes mouse movement and interaction time. Use realistic delays and emulate mouse movement via page.mouse.move before clicking the widget.
Script works in visible mode but fails in headless This is headless mode detection by the anti-bot system. Use masking libraries (e.g., playwright-stealth) or pass high-quality residential proxies to the API.

Conclusion

Automating GeeTest bypass in Playwright boils down to a clear understanding of the captcha version and correct parameter extraction. For v3, it is critically important to intercept a fresh challenge before every task, as this token is single-use. For v4, it is enough to find the static captcha_id once and reuse it.

After receiving the solution from the API, the key moment is the correct injection of tokens into the form. For v3, this means three fields (challenge, validate, seccode); for v4, it means five fields (lot_number, pass_token, gen_time, captcha_output, captcha_id). Many sites also expect a JavaScript callback to be triggered, which unlocks the form submission button.

By following this guide, you will be able to reliably bypass both versions of GeeTest, turning the captcha from a source of errors into a predictable stage of your automation.