Was this helpful?
How to Bypass Cloudflare Turnstile with Playwright
Technical engineer
Cloudflare Turnstile is a modern, privacy-friendly alternative to traditional captchas. Instead of asking users to click on pictures, it silently analyzes browser signals, behavior, and IP reputation to tell humans from bots. Most of the time it resolves invisibly — but for automation scripts, QA testing, and web scraping, Turnstile can still block your Playwright script from reaching the page it needs.
This tutorial shows how to bypass Cloudflare Turnstile in Playwright using the 2Captcha API: find the widget, get a solved token from 2Captcha, inject it into the page, and continue automating as if a real user had passed the check.
What you'll need
- Node.js or Python installed
- Playwright installed for your language
- A 2Captcha account and API key (get one here)
- The target page's URL and, if the widget isn't in proxyless mode, a working proxy
There are two different cases you may run into:
- Standalone Turnstile widget — the captcha is embedded directly in a page (for example, in a signup or login form). This is the case covered in this tutorial.
- Cloudflare Challenge page — Cloudflare replaces the entire page with a verification screen before letting the browser through. This requires extra parameters (cData, chlPageData, action) and is covered in a separate tutorial.
Step 1. Install the dependencies
For Python:
bash
pip install playwright 2captcha-python
playwright install chromium
For Node.js:
bash
npm install playwright @2captcha/captcha-solver
npx playwright install chromium
Step 2. Launch the browser and open the target page
Start with a normal Playwright session. Keep headless: False while you're building and testing the script — it's much easier to see what's happening on the page.
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/cloudflare-turnstile")
page.wait_for_selector(".cf-turnstile")
Step 3. Extract the Turnstile sitekey
Every Turnstile widget carries its sitekey in the data-sitekey attribute of the .cf-turnstile container. You can pull it straight out of the DOM with Playwright's get_attribute:
python
sitekey = page.locator(".cf-turnstile").get_attribute("data-sitekey")
page_url = page.url
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")
If the widget is rendered dynamically through JavaScript (turnstile.render(...)) rather than a static div, search the page source or intercept the network request to challenges.cloudflare.com instead — the sitekey is passed there as a query parameter.
Step 4. Send the sitekey to 2Captcha and get a token
With the sitekey and page URL in hand, send them to 2Captcha. The Python SDK handles the create-task / poll-result cycle for you:
python
from twocaptcha import TwoCaptcha
solver = TwoCaptcha('YOUR_API_KEY')
result = solver.turnstile(
sitekey=sitekey,
url=page_url
)
token = result['code']
print(f"Received token: {token}")
This calls the turnstile method of the official 2captcha-python SDK, which sends a Turnstile task to the 2Captcha API and polls for the result until it's solved. Solving typically takes 10–20 seconds. If the widget requires extra parameters (action, data/cData, pagedata), pass them as additional keyword arguments — solver.turnstile(sitekey=sitekey, url=page_url, action='login', data='...').
If you're calling the raw API instead of the SDK, the request looks like this (proxyless mode, used when the widget doesn't need a proxy tied to a specific IP):
bash
curl -X POST https://api.2captcha.com/createTask \
-H "Content-Type: application/json" \
-d '{
"clientKey": "YOUR_API_KEY",
"task": {
"type": "TurnstileTaskProxyless",
"websiteURL": "https://2captcha.com/demo/cloudflare-turnstile",
"websiteKey": "3x00000000000000000000FF"
}
}'
If you need a proxy tied to the task, use type: "TurnstileTask" instead and add proxyType, proxyAddress, proxyPort, proxyLogin, proxyPassword fields, as documented in the Cloudflare Turnstile API reference.
Step 5. Inject the token into the page
Once you have the token, it needs to end up in the hidden input Turnstile creates — usually . Since this field doesn't accept typed input, set its value directly through page.evaluate:
python
page.evaluate(f"""
document.querySelector('input[name="cf-turnstile-response"]').value = "{token}";
""")
Some pages don't just check the input value — they expect the widget's callback function to fire, since that's what enables the "Submit" button or triggers additional client-side logic. If a callback is configured, call it explicitly instead of only setting the input:
python
page.evaluate(f"""
if (window.turnstileCallback) {{
window.turnstileCallback("{token}");
}}
""")
You can check what the callback function is called by looking at the data-callback attribute on the .cf-turnstile element, or by inspecting the turnstile.render() call in the page's JavaScript.
Step 6. Continue the automation
At this point the page believes a human has completed the challenge. Submit the form or continue navigating normally:
python
page.click("button[type='submit']")
page.wait_for_load_state("networkidle")
browser.close()
Full working example (Python)
python
from playwright.sync_api import sync_playwright
from twocaptcha import TwoCaptcha
API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://2captcha.com/demo/cloudflare-turnstile"
solver = TwoCaptcha(API_KEY)
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto(TARGET_URL)
page.wait_for_selector(".cf-turnstile")
sitekey = page.locator(".cf-turnstile").get_attribute("data-sitekey")
result = solver.turnstile(sitekey=sitekey, url=TARGET_URL)
token = result['code']
page.evaluate(f"""
document.querySelector('input[name="cf-turnstile-response"]').value = "{token}";
if (window.turnstileCallback) {{
window.turnstileCallback("{token}");
}}
""")
page.click("button[type='submit']")
page.wait_for_load_state("networkidle")
browser.close()
Node.js version (Playwright + JavaScript)
javascript
const { chromium } = require('playwright');
const Captcha = require('@2captcha/captcha-solver');
const solver = new Captcha.Solver('YOUR_API_KEY');
const targetUrl = 'https://2captcha.com/demo/cloudflare-turnstile';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(targetUrl);
await page.waitForSelector('.cf-turnstile');
const sitekey = await page.locator('.cf-turnstile').getAttribute('data-sitekey');
const result = await solver.cloudflareTurnstile({
pageurl: targetUrl,
sitekey: sitekey
});
const token = result.data;
await page.evaluate((token) => {
document.querySelector('input[name="cf-turnstile-response"]').value = token;
if (window.turnstileCallback) {
window.turnstileCallback(token);
}
}, token);
await page.click("button[type='submit']");
await page.waitForLoadState('networkidle');
await browser.close();
})();
Troubleshooting
- Token gets rejected / form doesn't submit — double-check that you're setting the value on the correct input name, and that the callback (if one exists) is actually being invoked.
- Sitekey not found — the widget may be lazy-loaded inside an iframe added after page load; wait for the network request to challenges.cloudflare.com/turnstile and read the sitekey from its query string instead of the DOM.
- Works with headless: False but fails with headless: True — Cloudflare's risk scoring also looks at browser fingerprints. Pair the solved token with a proxy and a realistic user agent, and consider using playwright-extra with a stealth plugin to reduce headless-detection signals.
- Getting a token but still redirected to a Challenge page — you're dealing with the Cloudflare Challenge page case, not a standalone widget. That flow needs the cData/chlPageData/action parameters described in the Cloudflare Challenge tutorial.