Solve hCaptcha in Scrapling.

Scrapling fetches the page; the NoneCap hCaptcha solver returns the token. Read the data-sitekey in a page_action, get a real token with the NoneCap Python SDK, write it into the form's h-captcha-response field, then submit. The same flow works without a browser through Scrapling's Fetcher.

The four steps

The flow is the same on every page that gates a form with hCaptcha:

  • 1. Read the data-sitekey and the page url.
  • 2. Solve with nc.solve(), which submits the job and long-polls until the token is ready.
  • 3. Inject the token into textarea[name="h-captcha-response"] (in a browser) or the form body (without one), and fire the widget callback if there is one.
  • 4. Submit the form the way the page does. Tokens are single-use and expire in about two minutes, so get one right before you submit.

Install

Scrapling's browser fetchers live in its fetchers extra, and scrapling install downloads the browser they drive. The official nonecap SDK is fully typed, its only dependency is httpx, and it runs on Python 3.9 or newer:

Shell
pip install "scrapling[fetchers]" nonecap
scrapling install

Full example: StealthyFetcher with page_action

page_action takes a function that receives the live browser page after navigation. That is where the four steps run. The function also sends the browser's own user agent with the solve, which some sites need before they accept the token.

Python · Scrapling StealthyFetcher
import os

from nonecap import NoneCap
from scrapling.fetchers import StealthyFetcher

nc = NoneCap(api_key=os.environ["NONECAP_KEY"])

# Writes the token into the hCaptcha response fields. hCaptcha's drop-in mode
# reuses reCAPTCHA's old g-recaptcha-response field name, so it is set too when
# present; the value is still the hCaptcha token. Invisible widgets submit
# from data-callback, so the callback is fired if the page has one.
INJECT_JS = """(token) => {
    for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
        const el = document.querySelector('textarea[name="' + name + '"]');
        if (el) el.value = token;
    }
    const cb = document.querySelector("[data-callback]")
        ?.getAttribute("data-callback");
    if (cb && typeof window[cb] === "function") window[cb](token);
}"""

result = {}


def solve_hcaptcha(page):
    try:
        widget = page.locator("[data-sitekey]")
        if widget.count() == 0:
            return  # no hCaptcha on this page

        # 1. Read the sitekey off the rendered widget.
        sitekey = widget.first.get_attribute("data-sitekey")

        # 2. Get a real token. nc.solve() submits the job and long-polls
        #    until it reaches a terminal state.
        solve = nc.solve(
            type="hcaptcha",
            sitekey=sitekey,
            url=page.url,
            user_agent=page.evaluate("navigator.userAgent"),
        )

        # 3. Inject it into the page.
        page.evaluate(INJECT_JS, solve.token)

        # 4. Submit and wait for the next page to load.
        with page.expect_navigation():
            page.click("button[type=submit]")
        result["solve"] = solve
    except Exception as exc:
        result["error"] = exc


response = StealthyFetcher.fetch(
    "https://target.example/login",
    page_action=solve_hcaptcha,
    network_idle=True,
)
if "error" in result:
    raise result["error"]

# response is the page after the form went through.
print(response.status, response.url)
print(response.css("h1::text").get())

Scrapling logs an exception raised inside page_action and still returns the page, so the example keeps the exception and raises it after fetch(). When a solve fails, nc.solve() raises SolveFailedError with the full solve object on .solve, and no credits are charged. The other terminal cases get their own types too: InsufficientCreditsError, RateLimitError, and SolveTimeoutError, so you can catch exactly what you want to retry.

Many pages in one browser: StealthySession

StealthyFetcher.fetch() opens and closes a browser per call. For a crawl, keep one open with StealthySession and pass the same page_action to each fetch():

Python · StealthySession
from scrapling.fetchers import StealthySession

# One browser for the whole run; solve_hcaptcha only acts on pages
# that render a widget.
with StealthySession(network_idle=True) as session:
    for url in ["https://target.example/login", "https://target.example/signup"]:
        result.clear()
        response = session.fetch(url, page_action=solve_hcaptcha)
        if "error" in result:
            raise result["error"]
        print(url, response.status)

Without a browser: FetcherSession

When the form is a plain HTML POST, you don't need a browser at all. Scrapling's FetcherSession keeps cookies between the GET and the POST, its selectors read the sitekey and the form's hidden fields, and the token goes into the form body:

Python · Scrapling FetcherSession
import os

from nonecap import NoneCap
from scrapling.fetchers import FetcherSession

nc = NoneCap(api_key=os.environ["NONECAP_KEY"])

with FetcherSession(impersonate="chrome") as session:
    page = session.get("https://target.example/signup")

    # 1. Read the sitekey, plus the form's own fields (CSRF token and so on).
    sitekey = page.css("[data-sitekey]::attr(data-sitekey)").get()
    fields = {
        field.attrib["name"]: field.attrib.get("value", "")
        for field in page.css("form input[name]")
    }
    fields["email"] = "you@example.com"

    # 2. Get a real token.
    solve = nc.solve(type="hcaptcha", sitekey=sitekey, url=page.url)

    # 3. The token goes in the form body under both field names.
    fields["h-captcha-response"] = solve.token
    fields["g-recaptcha-response"] = solve.token

    # 4. Submit with the solve's user agent: some sites refuse a token
    #    sent from a different browser version.
    action = page.urljoin(page.css("form::attr(action)").get(""))
    headers = {"User-Agent": solve.user_agent} if solve.user_agent else {}
    submitted = session.post(action, data=fields, headers=headers)
    print(submitted.status, submitted.css("h1::text").get())

Reading the sitekey when it isn't a DOM attribute

The examples assume the widget renders a [data-sitekey] element, which is the common case. Some pages pass the key straight to hcaptcha.render() instead. On any Scrapling response, fall back to the sitekey query param on the hcaptcha.com/1/api.js script tag:

Sitekey fallback
from urllib.parse import parse_qs, urlparse

# 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:
sitekey = page.css("[data-sitekey]::attr(data-sitekey)").get()
if sitekey is None:
    src = page.css("script[src*='hcaptcha.com/1/api.js']::attr(src)").get()
    sitekey = parse_qs(urlparse(src).query)["sitekey"][0]

Don't hard-code the sitekey: sites rotate them, so read it from the live page each time. The sitekey explainer covers the other places it can hide.

Enterprise rqdata

Enterprise sitekeys bind each challenge to a fresh rqdata blob. Capture the rqdata the page passes into hcaptcha.render() / execute() and forward it with the enterprise type; the injection and submit steps stay the same:

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=page.url,
    rqdata=rqdata,  # the fresh, IP-bound challenge blob
)

Async: AsyncStealthySession

For concurrent pages, pair Scrapling's AsyncStealthySession with the SDK's AsyncNoneCap. The page_action becomes a coroutine, and max_pages sets how many tabs the session runs at once. Your NoneCap plan's concurrency cap still applies to the solves.

Python · asyncio
import asyncio
import os

from nonecap import AsyncNoneCap
from scrapling.fetchers import AsyncStealthySession

nc = AsyncNoneCap(api_key=os.environ["NONECAP_KEY"])
errors = []


async def solve_hcaptcha(page):
    try:
        widget = page.locator("[data-sitekey]")
        if await widget.count() == 0:
            return
        sitekey = await widget.first.get_attribute("data-sitekey")
        solve = await nc.solve(
            type="hcaptcha",
            sitekey=sitekey,
            url=page.url,
            user_agent=await page.evaluate("navigator.userAgent"),
        )
        await page.evaluate(INJECT_JS, solve.token)  # INJECT_JS from the full example
        async with page.expect_navigation():
            await page.click("button[type=submit]")
    except Exception as exc:
        errors.append(exc)  # Scrapling only logs it, as in the sync example


async def main(urls):
    async with AsyncStealthySession(network_idle=True, max_pages=5) as session:
        pages = await asyncio.gather(
            *(session.fetch(url, page_action=solve_hcaptcha) for url in urls)
        )
    if errors:
        raise errors[0]
    return pages

Scrapling's MCP server and AI agents

If an AI agent drives Scrapling through its MCP server, give the agent NoneCap as one more tool. When the agent lands on an hCaptcha gate, it reads the sitekey from the page, calls the tool, writes the returned token into h-captcha-response and submits, the same four steps as above. The AI agents guide has a ready tool schema and a Python handler for it.

When this isn't the right approach

NoneCap solves hCaptcha only: regular, invisible, and enterprise rqdata. If the page is protected by reCAPTCHA or Cloudflare Turnstile, this technique doesn't apply; those are on the roadmap, not live. And if the page has no captcha at all, you don't need a solver: let Scrapling fetch it and skip the token step.

Using Scrapy, Playwright or Selenium elsewhere in the same project? The flow is the same. See the Scrapy middleware, Playwright guide or Selenium guide. Request and response shapes for every call are in the API reference.

Last updated November 2026.

Frequently asked

Does Scrapling solve hCaptcha on its own?
No. StealthyFetcher's solve_cloudflare option is for Cloudflare's Turnstile and interstitial challenges, not hCaptcha. Scrapling's browser gets you a page that loads like a real visitor's, but an hCaptcha-gated form still wants a valid token in h-captcha-response before it goes through. NoneCap returns that token; your Scrapling code writes it into the page and submits.
Do I need StealthyFetcher, or is Fetcher enough?
The token is just a string, so a plain FetcherSession is enough when the form is a normal HTML POST: read the sitekey with page.css(), put the token in the form body as h-captcha-response, and post it. Use StealthyFetcher or StealthySession when the page builds the form with JavaScript, submits through an invisible widget's callback, or needs a real browser for the rest of the job.
Why did my page_action fail without an error?
Scrapling logs an exception raised inside page_action and still returns the page. That is why the example stores the exception and raises it after fetch() returns: a SolveFailedError or InsufficientCreditsError from the NoneCap SDK then reaches your code instead of only the log.
How much does each solve cost?
At least one credit per solve. Billing starts at one credit per hCaptcha challenge round (hCaptcha decides how many: often one, sometimes two or three), charged only on success, so failed solves are never charged. Credits run $0.20 to $0.50 per 1,000 depending on your pack, you get 1,300 free credits on signup, and no subscription is required. See pricing.

Start solving hCaptcha in minutes.

1,300 free credits on signup. Pay per solve, credits never expire, failed solves never charged.