Was this helpful?
How to bypass GeeTest with Playwright
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
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
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
# 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
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
# 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
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
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
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
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
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.
Useful Links
- 2Captcha API documentation for GeeTest: https://2captcha.com/api-docs/geetest
- GeeTest v3 demo page: https://2captcha.com/demo/geetest
- GeeTest v4 demo page: https://2captcha.com/demo/geetest-v4
- 2Captcha Python SDK on GitHub: https://github.com/2captcha/2captcha-python