DEV Community

Peak Fo
Peak Fo

Posted on Originally published at blog.peak.fo

How to Solve Cloudflare Turnstile in Selenium (Python)

Cross-posted from blog.peak.fo. I work on Peak, a Cloudflare Turnstile solving API, so the code below calls it. The automation-fingerprint problem it solves is the same no matter which solver you use.


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 carries 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 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.

The options, compared

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 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 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. undetected-chromedriver raises your pass rate; it doesn't settle the outcome, and that rate drops fast once you run unattended, at volume, or from a flagged IP range.

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 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.

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.

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The response comes back shaped like this:

{
  "success": true,
  "data": { "token": "0.Xy7Nm2..." },
  "cost": 0.0009
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 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",
}
Enter fullscreen mode Exit fullscreen mode

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.

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.

If you're on Node or driving a browser from JavaScript, the same pattern ports over to Playwright and Puppeteer. For the framework-agnostic version of the solve flow, see bypassing Cloudflare Turnstile with Python.

Start free at peak.fo. 1,000 free solves, no card, pay only for solves that land.

Top comments (0)