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-sitekey and the page url.
  • 2. Solve by calling POST /v1/solves (the Python SDK long-polls for you; raw HTTP can block with ?wait=N or go async with a webhook_url).
  • 3. Inject the returned token into textarea[name="h-captcha-response"] via driver.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:

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

Python · Selenium
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:

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

hCaptcha sitekey types and the matching NoneCap request
Sitekey typeHow you spot itWhat to send NoneCap
CheckboxA visible "I am human" widget renders; a textarea[name="h-captcha-response"] appears in the formtype: "hcaptcha" + sitekey + url
InvisibleNo widget; hcaptcha.execute() runs on an action and a data-callback handles the tokentype: "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 · rqdata
# 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?
No. undetected-chromedriver and selenium-stealth patch 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?
When the widget is in the DOM, use 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?
No. The 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.
How much does each solve cost?
At least one credit per solve. Billing is one credit per hCaptcha challenge round (hCaptcha decides how many: often one, sometimes two or three), charged only on success, and failed solves are auto-refunded. Credits run $0.40 to $0.50 per 1,000 depending on your pack, you get 100 free credits on signup, and there is no subscription. See pricing.

Start solving hCaptcha in minutes.

100 free credits on signup. Pay per solve, credits never expire, failed solves auto-refunded.