How to Solve Cloudflare Turnstile in Selenium (Python)

Selenium gets blocked by Cloudflare Turnstile because its webdriver fingerprint fails the token check. Here's the working fix with real Python code.

You point Selenium at a page, the login form loads, and sitting on top of it is a Cloudflare Turnstile widget that either spins forever or flips to an error the moment your driver touches the tab. Your script worked yesterday against a staging box with no challenge. Now it's dead in the water. The widget isn't broken, and neither is your code. Selenium is just carrying a fingerprint that Turnstile is built to catch, and a stock WebDriver browser is close to the worst-case profile you can hand it.

Why Selenium specifically trips Turnstile

Turnstile doesn't make you click a bus-in-a-grid puzzle. It runs a batch of browser checks in the background, scores how human the client looks, and issues a token if the score passes. Two things about Selenium push that score the wrong way.

First, the automation fingerprint. A browser driven by WebDriver advertises itself. navigator.webdriver returns true, the Chrome DevTools Protocol leaves traces, the default window has no real profile or history behind it, and the Chrome-for-Testing binaries that Selenium Manager pulls down have their own tells. Turnstile's challenge script reads all of that. None of it is illegal or hidden, it's just a clean signal that a program is driving the page.

Second, and this is the part people miss, Turnstile wants to hand your browser a token, and it won't issue one to a client it has already flagged. So you get a widget that renders, maybe even shows a spinner, and then silently refuses to produce a cf-turnstile-response value. There's nothing to click and nothing to retry. The score decided the outcome before you got a chance. If you want the mechanics of what that token actually is and how the server checks it, we wrote that up separately in the cf-turnstile-response token explained.

The options, honestly

There are three realistic paths, and they're not equal.

undetected-chromedriver

undetected-chromedriver patches the driver to strip the obvious automation flags, so navigator.webdriver reads false and a lot of the CDP noise goes quiet. It genuinely helps, mostly with the full-page interstitial, the "Checking your browser" screen Cloudflare throws before the site loads. On a decent residential IP, undetected-chromedriver will often get you past that interstitial without any extra work.

Where it falls down is the embedded widget, the .cf-turnstile box sitting inside a login or signup form. Clearing the interstitial and getting the widget to mint a token are different problems. The widget scores you again, in context, and a patched driver on a datacenter IP still looks like a patched driver. So undetected-chromedriver gets you a pass rate, not a guarantee, and that rate drops the moment you run unattended, at volume, or from a flagged IP range. Treat it as a tool that raises your odds, not one that settles the outcome.

Fighting the fingerprint harder

You can keep layering: real residential proxies, persistent profiles, human-like mouse movement, stealth patches on top of stealth patches. This can work, but it's a maintenance treadmill. Cloudflare ships a change, your pass rate craters, and you're back reverse-engineering the challenge on a Tuesday night. Fine as a hobby, rough as a dependency.

Solving the token with an API

The approach that actually stays stable: stop asking the flagged browser to produce the token, and request it from a solving service instead. The service solves against the page's sitekey, hands you a valid token, and you inject that token into the form yourself. Cloudflare's server reads cf-turnstile-response the same way no matter how it got populated. A token solved by the API and a token minted by a real click are indistinguishable at the point of validation. That's the whole trick, and it's why this holds up when fingerprint tricks don't.

The rest of this is the Selenium walkthrough for that third path.

A full working Selenium + Python walkthrough

You need two things off the page before you can solve: the page URL and the widget's sitekey. The sitekey lives in the data-sitekey attribute on the .cf-turnstile element. If you're not sure how to pull it reliably, or it's rendered dynamically, see how to find a Cloudflare Turnstile sitekey.

Step 1: read the sitekey from the live page

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

URL = "https://target.com/login"

driver = webdriver.Chrome()
driver.get(URL)

# Wait for the widget to render, then read its sitekey
widget = WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".cf-turnstile"))
)
sitekey = widget.get_attribute("data-sitekey")
print("sitekey:", sitekey)

If .cf-turnstile isn't present, the site may be using the explicit-render JavaScript API instead of the auto-render HTML attribute. In that case the sitekey is passed to turnstile.render() in the page's own script, so grep the page source for sitekey rather than relying on the element attribute.

Step 2: POST the sitekey to Peak

Send the URL and sitekey to the solve endpoint. For most cases the proxyless task type is all you need.

import requests

def solve_turnstile(url, sitekey):
    resp = requests.post(
        "https://api.peak.fo/solve",
        headers={"X-API-Key": "pk_your_api_key"},
        json={
            "task_type": "turnstiletaskproxyless",
            "url": url,
            "sitekey": sitekey,
        },
        timeout=30,
    ).json()
    if not resp.get("success"):
        raise RuntimeError(f"solve failed: {resp.get('error')}")
    return resp["data"]["token"]

token = solve_turnstile(URL, sitekey)  # ~1s

The response comes back shaped like this:

{
  "success": true,
  "data": { "token": "0.Xy7Nm2..." },
  "cost": 0.0009
}

You pay per successful solve. A failed solve costs nothing, so you're not charged for the misses.

Step 3: inject the token and fire the callback

Now drop the token into the cf-turnstile-response textarea that Turnstile injects into the form. Most pages read this field directly on submit.

driver.execute_script("""
    let el = document.querySelector('[name="cf-turnstile-response"]');
    if (!el) {
        el = document.createElement('textarea');
        el.name = 'cf-turnstile-response';
        el.style.display = 'none';
        (document.forms[0] || document.body).appendChild(el);
    }
    el.value = arguments[0];
""", token)

Setting the field is enough for a plain form POST. But a lot of sites keep the submit button disabled until Turnstile fires its success callback, the function named in the widget's data-callback attribute. If the button stays greyed out after you set the field, read that attribute and call the function directly:

callback = widget.get_attribute("data-callback")
if callback:
    driver.execute_script(f"window['{callback}'](arguments[0]);", token)

Step 4: submit

driver.find_element(By.NAME, "email").send_keys("you@example.com")
driver.find_element(By.NAME, "password").send_keys("...")
driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()

The form posts with a token Cloudflare already validated. No widget to wait on, no spinner to babysit.

Handling the invisible variant

Turnstile has a managed mode that renders no visible checkbox. There's still a .cf-turnstile element in the DOM, it's just zero-size, so your get_attribute("data-sitekey") call works exactly the same. The widget is still waiting on a token it won't issue to a flagged browser, and the fix is identical: solve against the sitekey, inject the response field, fire the callback if one exists.

The one thing that changes with invisible Turnstile is the callback matters more. Visible widgets often gate on the field; invisible ones almost always gate on the JavaScript callback, because there's no checkbox for a user to tick. So if the invisible variant isn't submitting, check the callback path first.

When to pass your own proxy

The proxyless task works when the target scores the token on its own merits. Some sites tie the token to the IP that requested it, and if your Selenium session submits from a different IP than the one Peak solved from, validation fails. That's when you switch to the proxied task type and hand over the same proxy your browser uses:

json={
    "task_type": "turnstiletask",
    "url": url,
    "sitekey": sitekey,
    "proxy": "http://user:pass@ip:port",
}

Rule of thumb: start proxyless. If tokens solve fine but the target rejects the submit, move to the proxied task and match the IP. Point Selenium at the same proxy with --proxy-server so the solve and the submit come from one address.

Common mistakes

  • Solving too early. Turnstile tokens are single-use and expire in roughly 300 seconds. Request the token right before you submit, not at the top of a long script.
  • Setting an input instead of a textarea. Turnstile uses a textarea named cf-turnstile-response. If you create an input and the page's own validation checks the node type, it can reject it. Match what Turnstile itself injects.
  • Ignoring the callback. If the submit button won't enable, the site is gating on data-callback, not the field. Call the function.
  • Mismatched IP. Solve through the same proxy you submit from when the target binds the token to the originating address.
  • Expecting undetected-chromedriver to do the whole job. It helps with the interstitial. It does not reliably mint widget tokens under automation.

Where this fits

If you're doing QA on your own flows, scraping public data, or automating a legitimate task, injecting a solved token is the stable way through Turnstile in Selenium. Keep it to what you're allowed to do, respect the site's terms and robots rules, and don't point this at credential stuffing or anything that abuses the target. The technique is about not getting blocked while doing legitimate automation, not about breaking in.

If you're on Node or driving a browser from JavaScript, the same pattern ports over in handling Turnstile in Playwright and Puppeteer. For the framework-agnostic version of the solve flow, there's how to bypass Cloudflare Turnstile with Python. And the full copy-paste reference lives at the Cloudflare Turnstile Python solver page.

FAQ

Can Selenium solve Cloudflare Turnstile on its own?

Not reliably. A stock WebDriver browser carries automation signals that drop the Turnstile score below the pass line, so the widget often refuses to issue a token at all. undetected-chromedriver improves the odds, mainly on the full-page interstitial, but the embedded widget still scores you again and the pass rate falls under automation. Solving the token through an API and injecting it is the dependable route.

Does undetected-chromedriver bypass Turnstile?

It helps with the "Checking your browser" interstitial and can clear it on a good IP. It does not reliably make the embedded .cf-turnstile widget produce a token when the browser is being driven, especially from datacenter IPs or at volume. Use it to raise your pass rate, not as a guarantee.

What's the difference between the proxyless and proxied task types?

turnstiletaskproxyless solves without a proxy and works when the target scores the token independently of the requesting IP. turnstiletask takes a proxy field and is for sites that bind the token to the IP that solved it, so the solve and the submit come from the same address.

Why does the submit button stay disabled after I set the response field?

The site is gating on Turnstile's success callback rather than the field value. Read the widget's data-callback attribute and call that function with the token via execute_script.

What does this cost?

Peak charges $0.90 per 1,000 successful Turnstile solves, dropping to $0.35 per 1,000 at volume. Failed solves are free, and new accounts get 1,000 free solves with no card. See pricing.

Getting blocked by Turnstile in Selenium? Grab a key free at peak.fo and solve your first 1,000 on the house.