Solve hCaptcha in Selenium (Python).
Don't fight the challenge inside the browser you're automating. Read the
data-sitekey off the page with find_element, ask NoneCap for a
real token, write it into the form's h-captcha-response field with
execute_script, then submit. Four steps, all in plain Python.
Why undetected-chromedriver alone doesn't get you a token
undetected-chromedriver and selenium-stealth do one job well: they patch
navigator.webdriver and the other obvious automation tells so the crudest
bot checks stop firing. That isn't the same as getting a valid hCaptcha token. hCaptcha
scores the whole session: the TLS and browser fingerprint, the way input
arrives, the reputation of the IP you're on. It only hands out a token when that score
looks human, and a patched WebDriver session still reads as a patched WebDriver session.
Enterprise sitekeys go further and bind every challenge to a fresh rqdata
blob tied to your request.
So stop solving in-browser. NoneCap solves it out of band, off your
machine, and returns a normal P1_ token. Your Selenium script never opens the
challenge UI. It just supplies the token the page was already waiting for.
The four steps
The flow is the same shape on every page that gates with hCaptcha:
- 1. Read the
data-sitekeyand the pageurl. - 2. Solve by calling
POST /v1/solves(the Python SDK long-polls for you; raw HTTP can block with?wait=Nor go async with awebhook_url). - 3. Inject the returned token into
textarea[name="h-captcha-response"]viadriver.execute_script, and fire the widget callback if there is one. - 4. Submit the form the way your page does.
Full Python example
The example uses Selenium 4 and the official
nonecap SDK. It's fully
typed, its only dependency is httpx, and it runs on Python 3.9 or newer:
pip install nonecap nc.solve() submits the job and long-polls until it reaches a terminal state,
so the token comes back from one call with no polling loop on your side.
Prefer raw HTTP? Plain requests or httpx against the same
endpoint works fine; the request and response shapes are in the
API reference.
import os
from nonecap import NoneCap
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
nc = NoneCap(api_key=os.environ["NONECAP_KEY"])
driver = webdriver.Chrome()
driver.get("https://target.example/login")
# 1. Read the sitekey straight off the rendered widget.
sitekey = driver.find_element(
By.CSS_SELECTOR, "[data-sitekey]"
).get_attribute("data-sitekey")
# 2. Mint a real token out of band. nc.solve() submits the job and
# long-polls until it reaches a terminal state.
solve = nc.solve(type="hcaptcha", sitekey=sitekey, url=driver.current_url)
token = solve.token # a real P1_... token
# 3. Inject the token. The h-captcha-response textarea lives in the top
# document, not inside the widget iframe, so no frame switching.
# hCaptcha's drop-in mode reuses reCAPTCHA's old g-recaptcha-response
# field name, so we set it too if present. The value is still the
# hCaptcha token, not a reCAPTCHA solve.
driver.execute_script("""
const token = arguments[0];
for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
const el = document.querySelector(`textarea[name="${name}"]`);
if (el) el.value = token;
}
// Invisible widgets submit from data-callback, fire it if present.
const cb = document.querySelector("[data-callback]")
?.getAttribute("data-callback");
if (cb && typeof window[cb] === "function") window[cb](token);
""", token)
# 4. Submit the form however your page does it.
old_url = driver.current_url
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
WebDriverWait(driver, 15).until(EC.url_changes(old_url))
driver.quit()
When a solve fails, nc.solve() raises SolveFailedError with the
full solve object on .solve, and the credits are auto-refunded. The other
terminal cases get their own types too: InsufficientCreditsError,
RateLimitError, and SolveTimeoutError, so you can catch exactly
what you want to retry.
Reading the sitekey when it isn't a DOM attribute
The example assumes the widget renders a [data-sitekey] element, which is
the common case. Some pages instead pass the key straight to hcaptcha.render()
and never set the attribute. When find_element throws, fall back to the
sitekey query param on the hcaptcha.com/1/api.js script tag:
from urllib.parse import urlparse, parse_qs
from selenium.common.exceptions import NoSuchElementException
# Some pages never put data-sitekey in the DOM and pass the key straight
# to hcaptcha.render(). Read it off the api.js script tag instead:
try:
sitekey = driver.find_element(
By.CSS_SELECTOR, "[data-sitekey]"
).get_attribute("data-sitekey")
except NoSuchElementException:
src = driver.find_element(
By.CSS_SELECTOR, "script[src*='hcaptcha.com/1/api.js']"
).get_attribute("src")
sitekey = parse_qs(urlparse(src).query)["sitekey"][0] Don't hard-code the sitekey: sites rotate them, and enterprise deployments bind the challenge to the page, so always read it from the live DOM.
Checkbox vs invisible vs enterprise
The request you send NoneCap depends on which kind of hCaptcha the page renders. The
injection step is identical; what changes is the type you send and, for
invisible widgets, whether you need to fire the callback yourself.
| Sitekey type | How you spot it | What to send NoneCap |
|---|---|---|
| Checkbox | A visible "I am human" widget renders; a textarea[name="h-captcha-response"] appears in the form | type: "hcaptcha" + sitekey + url |
| Invisible | No widget; hcaptcha.execute() runs on an action and a data-callback handles the token | type: "hcaptcha" + sitekey + url, then fire the callback |
Enterprise (rqdata) | The page passes a fresh rqdata blob into hcaptcha.render() / execute() | type: "hcaptcha_enterprise" + sitekey + url + rqdata |
Checkbox sitekeys render a visible widget and a
h-captcha-response textarea; set the value and submit.
Invisible sitekeys have no widget and submit from a
data-callback, so after injecting you call window[callbackName](token)
inside the same execute_script, which the example above already does.
Enterprise rqdata
Enterprise hCaptcha is where in-browser tricks fall down hardest: each challenge carries
an IP-bound rqdata blob, so a token has to be minted in a real session that
matches it. Capture the rqdata the page passes into
hcaptcha.render() / execute() and forward it with the enterprise
type:
# Enterprise sitekeys also need the per-challenge rqdata blob. Capture
# the value the page passes into hcaptcha.render() / execute(), then
# forward it with the enterprise type:
solve = nc.solve(
type="hcaptcha_enterprise",
sitekey=sitekey,
url=driver.current_url,
rqdata=rqdata, # the fresh, IP-bound challenge blob
) A note on g-recaptcha-response
The example also sets textarea[name="g-recaptcha-response"] when it exists.
That is hCaptcha's drop-in compatibility field: hCaptcha reuses reCAPTCHA's old field
name so sites can swap providers without touching their backend. The value you write is
still the hCaptcha P1_ token, not a reCAPTCHA solve. NoneCap
does not solve reCAPTCHA.
Async instead of blocking
nc.solve() blocks until the solve finishes, which is the simplest path for a
single run. For high-throughput jobs, submit without waiting and pass a
webhook_url to receive a solve.completed callback, or poll
GET /v1/solves/{id} yourself. Concurrency is capped per account (5 on
the free trial, 25 on Starter, 50 on Builder, 100 on Scale), so a fleet of Selenium
workers can run in parallel up to that cap. The full object and every language sample
are in the API reference.
When this isn't the right approach
NoneCap solves hCaptcha only: regular, invisible, and enterprise
rqdata. If the page is actually protected by reCAPTCHA, Cloudflare Turnstile, or FunCaptcha, this technique doesn't apply; those are on the roadmap, not live. And if your target has no captcha at all, you don't need a solver. Drive the form directly in Selenium and skip the token step entirely.
Driving Playwright or Puppeteer instead of Selenium? The flow is identical. Read the
data-sitekey, mint the token, set h-captcha-response in the
DOM, and fire the callback. See
hCaptcha solving in Playwright or
hCaptcha solving in Puppeteer. And if you don't need a
browser at all, solve hCaptcha in Python covers the same
pipeline with plain HTTP clients, as does
hCaptcha for web scraping.
Last updated June 2026.
Frequently asked
Does undetected-chromedriver solve hCaptcha on its own?
navigator.webdriver and the other obvious automation tells, which gets you past basic bot checks. hCaptcha scores the whole session: TLS and browser fingerprint, input cadence, IP reputation, and on enterprise sitekeys each challenge is bound to a fresh rqdata blob. A patched WebDriver session still reads as one, so it never gets handed a token. NoneCap solves it separately, off your machine, and returns a finished token; your Selenium script only injects and submits.How do I read the sitekey in Selenium?
driver.find_element(By.CSS_SELECTOR, "[data-sitekey]").get_attribute("data-sitekey"). If the page passes the key straight to hcaptcha.render() and never sets the attribute, read the sitekey query param off the hcaptcha.com/1/api.js script tag with urllib.parse.parse_qs instead. Send that value plus driver.current_url to POST /v1/solves.Do I need to switch into the hCaptcha iframe to inject the token?
textarea[name="h-captcha-response"] lives in the top document, not inside the widget iframe, so a single driver.execute_script call reaches it without switch_to.frame. The one extra step is for invisible widgets: they don't submit on a value change, so after setting the textarea, look up the data-callback attribute and call window[name](token) from the same script, exactly as the example does.