Skip to content

Tutorial

Selenium and Cloudflare Turnstile: undetected-chromedriver vs API

What undetected-chromedriver and SeleniumBase UC Mode do about Cloudflare Turnstile, where they stop, and a Selenium fallback that gets a token from an API.

By 4 min readPublished Updated

undetected-chromedriver is a patched ChromeDriver that removes the traces that give away an automated Chrome, and SeleniumBase’s UC Mode builds on it, adding methods that click the Cloudflare Turnstile checkbox with real mouse input. Both raise the chance that the widget passes in your browser. Neither changes your IP address, both need a visible browser to work well, and Turnstile can still refuse. The dependable setup gives the widget a few seconds and then falls back to a token from a solving API. This article compares the tools and shows that fallback in Selenium.

Automate only sites you are allowed to. See responsible captcha automation.

undetected-chromedriver

  • What it is: a drop-in replacement for Selenium’s Chrome driver, import undetected_chromedriver as uc then uc.Chrome(), that patches the driver so anti-bot scripts do not see the usual webdriver traces.
  • What its README warns: it “DOES NOT hide your IP address”, so running from a data center makes detection much more likely, and headless mode is not officially supported.
  • Release: version 3.5.5, uploaded to PyPI on 17 February 2024 (checked 1 October 2026). Chrome has changed a great deal since, which is worth knowing before you depend on it.

SeleniumBase UC Mode

  • What it is: SeleniumBase’s mode “based on undetected-chromedriver”, with fixes and extra methods, in an actively released package (4.54.13 on PyPI, checked 1 October 2026).
  • uc_open_with_reconnect(url, seconds) opens a page while the driver is disconnected, so the page’s scripts cannot see it during load.
  • uc_gui_click_captcha() finds a Turnstile or reCAPTCHA checkbox and clicks it with PyAutoGUI, real operating-system mouse input rather than a WebDriver command.
  • Its caveats: its docs say “UC Mode is detectable in Headless Mode”, and PyAutoGUI needs a display; on a Linux server that means a virtual one (xvfb=True).

Where browser-side tools stop

Both tools work on the browser’s side of the check. Cloudflare decides on its side, from signals that, by its own privacy addendum, include the client IP address, the TLS fingerprint and the User-Agent header. Its testing docs add that “Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile”. Two consequences:

  1. A clean browser on a flagged network can still fail. The IP warning in the undetected-chromedriver README is the same limit, stated plainly.
  2. Clicking is not a token. When the widget stays unsolved, or shows an error, your code has no token to submit, however well the browser is disguised.

A solving API sits outside that contest: it returns a token for the page’s sitekey, and your code submits it. The two approaches combine well.

The fallback in Selenium

It needs Python 3.10 or later, as current Selenium and requests releases do. Install selenium and requests, set ZEROCAPTCHA_API and ZEROCAPTCHA_KEY, and save this as login.py. It uses plain Selenium; to use undetected-chromedriver, replace webdriver.Chrome() with uc.Chrome() and nothing else changes.

import os
import time
import uuid
import requests
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
API = os.environ["ZEROCAPTCHA_API"]
KEY = os.environ["ZEROCAPTCHA_KEY"]
def solve_turnstile(page_url, sitekey, action=None, cdata=None):
task = {"type": "TurnstileTaskProxyless", "websiteURL": page_url, "websiteKey": sitekey, "metadata": {}}
# The widget's data-action and data-cdata go in metadata, only when it sets them: many sites
# check both when they verify the token.
if action:
task["metadata"]["action"] = action
if cdata:
task["metadata"]["cdata"] = cdata
# One Idempotency-Key per task: a retried create with it returns the same task.
created = requests.post(f"{API}/createTask", json={"clientKey": KEY, "task": task},
headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=15).json()
if created["errorId"]:
raise RuntimeError(f"createTask: {created['errorCode']}")
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
time.sleep(2)
result = requests.post(
f"{API}/getTaskResult", json={"clientKey": KEY, "taskId": created["taskId"]}, timeout=15
).json()
if result["errorId"]:
raise RuntimeError(f"getTaskResult: {result['errorCode']}")
if result["status"] == "ready":
return result["solution"]["token"]
raise TimeoutError("no token within 180 seconds")
def widget_token(driver):
"""The token the widget itself wrote, or an empty string."""
return driver.execute_script(
"const input = document.querySelector('[name=\"cf-turnstile-response\"]');"
"return input ? input.value : '';"
)
driver = webdriver.Chrome()
try:
driver.get("https://shop.example.com/login")
widget = WebDriverWait(driver, 15).until(lambda d: d.find_element(By.CSS_SELECTOR, "[data-sitekey]"))
try:
# 1. Give the widget ten seconds to pass on its own.
WebDriverWait(driver, 10).until(widget_token)
except TimeoutException:
# 2. Otherwise, get a token from the API and put it where the widget would have.
token = solve_turnstile(
driver.current_url,
widget.get_attribute("data-sitekey"),
widget.get_attribute("data-action"),
widget.get_attribute("data-cdata"),
)
driver.execute_script(
"for (const input of document.querySelectorAll('[name=\"cf-turnstile-response\"]'))"
" input.value = arguments[0];",
token,
)
driver.find_element(By.ID, "email").send_keys("me@example.com")
driver.find_element(By.ID, "password").send_keys(os.environ.get("SHOP_PASSWORD", ""))
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
WebDriverWait(driver, 15).until(lambda d: "/login" not in d.current_url)
print("Logged in:", driver.current_url)
finally:
driver.quit()

WebDriverWait.until calls widget_token until it returns something non-empty, so the first wait ends as soon as the widget writes its token. Selenium 4.6 and later download a matching ChromeDriver on their own (Selenium Manager), so webdriver.Chrome() needs no setup.

Choosing

undetected-chromedriver SeleniumBase UC Mode Solving API fallback
Works headless Not officially Detectable, per its docs Yes: no browser needed for the token
Needs a display No For GUI clicks, yes No
Depends on your IP’s reputation Yes Yes The token does not; your form submission still comes from your IP
Cost Free Free Charged per solved token, only when the widget did not pass
Maintenance Last release February 2024 Active Maintained by the API provider

Using both keeps costs down when the widget passes on its own, and keeps the run going when it does not. A Cloudflare Turnstile token lasts 300 seconds and works once, so submit straight after the fallback: see Cloudflare Turnstile token expiry.

Sources

The team that builds and runs the ZeroCaptcha API. Articles are drafted with AI tools, then checked against the API's code and the primary sources each one cites.

Questions

Does undetected-chromedriver get past Cloudflare Turnstile?

It removes signals that give away an automated Chrome, which helps the widget pass, but its README warns that it does not hide your IP address and that headless mode is not officially supported. Turnstile can still refuse it.

What does SeleniumBase UC Mode add?

UC Mode builds on undetected-chromedriver and adds methods such as uc_gui_click_captcha, which clicks the Turnstile checkbox with real mouse input through PyAutoGUI. It needs a display, and its docs say UC Mode is detectable in headless mode.

Can I combine undetected-chromedriver with a CAPTCHA-solving API?

Yes. Let the widget try in the browser; if no token appears within a few seconds, get one from the API and write it into the cf-turnstile-response input. The Selenium code is the same with either driver.

Read next

This article is part of the Cloudflare Turnstile solver hub. Every task is charged only when a token is ready.

Get an API key