How to Handle CAPTCHA in undetected-chromedriver (Python)

undetected-chromedriver patches Chrome so the automation flags stop leaking. That gets you onto the page. It does not get you past a reCAPTCHA checkbox, because a widget is not a fingerprint check, it is a token requirement. An undetected-chromedriver captcha still has to be solved and the answer injected into the DOM.
Four steps: start the patched driver, read the sitekey off the page, solve locally, put the token where the page expects it. Here is all of it in Python.
What undetected-chromedriver does and does not do
| Signal | Handled by the driver |
|---|---|
navigator.webdriver and the CDP giveaways | Yes |
| Patched binary so the driver is not detected as one | Yes |
| A reCAPTCHA v2 checkbox on a login form | No |
| An invisible v2 or v3 challenge fired by the page | No |
| A Turnstile widget | No |
The distinction matters because the usual advice for a blocked scraper is “add stealth”. If a widget is on screen, no amount of stealth removes it. You need a token.
What you need
- Chrome installed, and CapSkip running on
127.0.0.1:8080. The setup guide covers the app. - Python 3.10 or newer.
- Three packages.
# the patched driver, selenium itself, and the local solver pip install undetected-chromedriver selenium capskip
Step 1: start the patched driver
Import it as uc and build the browser the same way you would with plain Selenium. Options come from uc.ChromeOptions(), not Selenium’s, because the library patches them on the way through:
# pip install undetected-chromedriver
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.add_argument("--window-size=1280,800")
# use_subprocess keeps the patched binary alive long enough
# to shut down cleanly on Windows.
driver = uc.Chrome(options=options, use_subprocess=True)
driver.get("https://example.com/login")Do not stack another stealth plugin on top of this. Two patchers fighting over the same properties is a detection signal in itself, and it is a common reason a setup that used to work suddenly stops.
Step 2: read the sitekey off the page
The sitekey is a public attribute on the widget element. Read it from the live DOM rather than hardcoding it, because plenty of sites rotate keys per environment:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# The widget is injected by script, so wait for it.
widget = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
)
sitekey = widget.get_attribute("data-sitekey")
page_url = driver.current_url
print(sitekey)Use driver.current_url rather than the URL you asked for. If the site redirected you to a challenge path, the solver has to be told about the page you actually landed on.
Step 3: solve it locally
CapSkip runs on your own machine, so the solve is a call to localhost. No account, no per-solve billing, nothing leaves the box except the sitekey and the page URL:
# pip install capskip
from capskip import CapSkip
solver = CapSkip(
host="127.0.0.1",
port=8080,
recaptchaTimeout=300, # seconds, also covers Turnstile
)
result = solver.recaptcha(sitekey=sitekey, url=page_url)
token = result["code"] # the g-recaptcha-response valueVariants are options rather than different methods. Add invisible=1 for an invisible v2 widget, version="v3" with an action for v3, and enterprise=1 for the Enterprise product.
Step 4: inject the token and fire the callback
The token has to land in the hidden g-recaptcha-response textarea. That alone is enough for a plain form submit, because the field is posted with the rest of the form:
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".innerHTML = arguments[0];",
token,
)
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()Single-page apps are different. They never read the textarea, they wait for the widget’s own callback to hand them the token. The function name is declared on the element, so you can call it directly:
callback = widget.get_attribute("data-callback")
if callback:
# Call the page's own handler with the token, exactly as
# the widget would have done.
driver.execute_script(f"{callback}(arguments[0]);", token)If nothing happens after injection and there is no data-callback, the page is wiring the callback up in JavaScript instead of markup. The reCAPTCHA v2 callback solver page covers how to find it in that case.
Turnstile in the same driver
Cloudflare Turnstile works the same way with two names changed. The response field is cf-turnstile-response, and it is an input rather than a textarea:
result = solver.turnstile(sitekey=sitekey, url=page_url)
driver.execute_script(
"document.querySelector('[name=cf-turnstile-response]')"
".value = arguments[0];",
result["code"],
)That covers a widget embedded in a form. A full-page interstitial challenge needs two extra values and the user agent that the solve was performed with, which the Turnstile solver page explains.
Full working example
# pip install undetected-chromedriver selenium capskip
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from capskip import (
CapSkip, NetworkException, ApiException, TimeoutException,
)
URL = "https://example.com/login"
solver = CapSkip(host="127.0.0.1", port=8080)
driver = uc.Chrome(options=uc.ChromeOptions(), use_subprocess=True)
try:
driver.get(URL)
widget = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
)
sitekey = widget.get_attribute("data-sitekey")
result = solver.recaptcha(sitekey=sitekey, url=driver.current_url)
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".innerHTML = arguments[0];",
result["code"],
)
callback = widget.get_attribute("data-callback")
if callback:
driver.execute_script(f"{callback}(arguments[0]);", result["code"])
else:
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
except NetworkException:
print("CapSkip is not running on 127.0.0.1:8080")
except (ApiException, TimeoutException) as err:
print(f"solve failed: {err}")
finally:
driver.quit() # always, or the patched binary lingersSolve and submit in one run. A reCAPTCHA token is accepted for roughly two minutes, so a script that solves, then does thirty seconds of other work, then submits, is fine. One that queues tokens for later is not.
Common errors
| What you see | Cause | Fix |
|---|---|---|
This version of ChromeDriver only supports Chrome version N | The patched driver guessed the wrong Chrome major version | Pass it explicitly: uc.Chrome(version_main=N) where N is your installed Chrome major version |
OSError: [WinError 6] The handle is invalid on exit | The driver was garbage collected before it shut down | Call driver.quit() in a finally block and keep use_subprocess=True |
NoSuchElementException when locating [data-sitekey] | The widget is in an iframe, or has not rendered yet | Wait for it as above. For an iframe, read the sitekey from the iframe src query string instead |
ERROR_GOOGLEKEY | An empty or truncated sitekey reached the solver | Print it before solving. See fixing ERROR_GOOGLEKEY |
NetworkException | CapSkip is not running, or the port differs | Start the app and check the port in its settings |
| Token injected, form still refused | The page uses a callback and never reads the textarea | Call data-callback with the token |
Every error code and parameter is listed in the API documentation.
Frequently asked questions
Does undetected-chromedriver solve CAPTCHAs by itself?
No. It hides the automation signals that get a browser flagged. A rendered widget still expects a valid token, so you solve it and inject the result. The two jobs are complementary, not alternatives.
Can I run it headless?
You can, with uc.Chrome(headless=True), which applies the library’s own headless patches rather than Selenium’s flag. Expect more challenges than in headed mode. Token injection itself works identically either way.
Do I need a proxy for the solve?
Only if the browser is already going through one. Pass the same one as proxy={"type": "HTTPS", "uri": "user:pass@host:port"} so the solve and the submit share a network path. Proxies apply to reCAPTCHA, Turnstile and GeeTest, never to image CAPTCHAs.
Does this work with plain Selenium too?
Yes. Steps 2 to 4 are ordinary WebDriver calls, so they run unchanged on standard Selenium, on Selenium Grid, and on remote drivers. Only step 1 is specific to the patched library.
Summary
Let undetected-chromedriver handle the fingerprint, and handle the widget yourself. Wait for [data-sitekey], solve against localhost, write the token into g-recaptcha-response, and call data-callback when the page declares one. Quit the driver in a finally block so the patched binary does not linger.
The wider Selenium surface is on the Selenium CAPTCHA solver page, the full method list is on the Python CAPTCHA solver page, and CAPTCHA solving for web scraping covers running this at volume. CapSkip is a local captcha solver, so every solve above happens on your own machine.
