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

- Source: https://zerocaptcha.io/blog/selenium-undetected-chromedriver-cloudflare
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

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](https://zerocaptcha.io/guides/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.

```python
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](https://zerocaptcha.io/guides/cloudflare-turnstile-token-expiry).

## Sources

- [undetected-chromedriver README](https://github.com/ultrafunkamsterdam/undetected-chromedriver)
  and [PyPI](https://pypi.org/project/undetected-chromedriver/) (checked 1 October 2026).
- [SeleniumBase UC Mode](https://seleniumbase.io/help_docs/uc_mode/) and
  [PyPI](https://pypi.org/project/seleniumbase/) (checked 1 October 2026).
- [Cloudflare Turnstile: testing](https://developers.cloudflare.com/turnstile/troubleshooting/testing/)
  and [privacy addendum](https://www.cloudflare.com/turnstile-privacy-policy/) (checked 1
  October 2026).
- [Selenium Manager](https://www.selenium.dev/documentation/selenium_manager/) (checked 1
  October 2026).

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