How to Solve CAPTCHAs in hrequests (Python TLS Client)

An hrequests captcha solve is short, because hrequests is not the part that reads the CAPTCHA. It sends requests through a Go TLS client so your handshake looks like a real Chrome or Firefox, and that gets you past the check that fires before any challenge is drawn. When the site draws one anyway, you need something that turns a sitekey into a token. The part worth getting right is what happens between them: the token has to go back on the same session that fetched the page, not on a fresh one.
What you need
- Python 3.10 or newer, with hrequests installed. Add the browser extra only if you actually need to render.
- CapSkip running on a Windows machine. Local mode if the script runs on that machine, Server mode if it runs anywhere else.
- The sitekey and the page URL. Step 1 pulls the sitekey out of the page rather than hardcoding it.
- A proxy, if the target cares about your IP as well as your handshake. hrequests takes one as a URL string.
# pip install capskip pip install -U hrequests capskip # Only if you need the browser. It downloads a browser build. pip install -U hrequests[all] python -m hrequests install
Two different blocks, two different tools
Worth being blunt about this, because the two get confused constantly. hrequests replicates browser TLS fingerprints and generates matching headers. It does not look at an image and it does not produce a reCAPTCHA token. CapSkip produces tokens and does not touch your TLS fingerprint. Being blocked before a challenge ever appears is a handshake problem, and the guide to TLS fingerprinting in Python is the write-up for that. Being shown a challenge is what the rest of this page is about.
One detail from the hrequests documentation is worth knowing before you pick a browser. Session creation takes a browser argument that accepts firefox or chrome, and the README’s prose and its parameter table disagree about which one you get by default. Pass it explicitly and the ambiguity goes away.
Step 1: fetch the page and pull the sitekey
Use a session rather than a bare get, because the session is what accumulates cookies and what you will post the token back through. hrequests ships a fast HTML parser on the response, so you can read the sitekey out of the markup instead of guessing it.
# pip install hrequests
import hrequests
SITE = "https://example.com/page-with-recaptcha"
# Name the browser. Headers are generated to match it and the OS.
session = hrequests.Session(browser="chrome", os="win")
resp = session.get(SITE)
# The parser is selectolax under the hood, so this is cheap.
widget = resp.html.find(".g-recaptcha")
sitekey = widget.attrs["data-sitekey"]
print(resp.status_code, sitekey)If the widget is not in the initial HTML, it is being injected by JavaScript and you need a render. Skip to the browser section below before reaching for one, though, because most reCAPTCHA and Turnstile widgets are in the served markup and rendering costs you a browser process for nothing.
Step 2: solve it with CapSkip
One call. The SDK submits the challenge and polls for the answer, starting at 250 milliseconds and backing off, which is why it usually beats a hand written loop against the raw API.
# pip install capskip from capskip import CapSkip # 127.0.0.1 only if this script runs on the solver machine. solver = CapSkip(host="127.0.0.1", port=8080) result = solver.recaptcha(sitekey=sitekey, url=SITE) token = result["code"] # the g-recaptcha-response value
Every other type CapSkip supports is the same shape. Add invisible or enterprise set to 1, or version set to v3 with an action name. Turnstile and GeeTest have their own methods, and Turnstile is the one to read the notes on, because a challenge page needs two extra values and the user agent that comes back with the token. Those extra values are covered on the Cloudflare Turnstile solver page. Every parameter for every type is listed in the CapSkip API documentation.
Step 3: post the token on the same session
This is the step people get wrong, and it is the whole reason to use hrequests rather than a plain HTTP client. The session that fetched the page has a TLS fingerprint, a generated header set and whatever cookies the site handed out. Post the token through that same session and the submission looks like it came from the same client. Post it through a fresh session, or worse through the standard library, and the handshake changes mid-flow, which is a signal in itself.
# Same session object, so the fingerprint, headers and cookies
# are the ones the site already saw on the GET.
posted = session.post(
SITE,
data={
"username": "YOUR_USERNAME",
"g-recaptcha-response": token,
},
timeout=30,
)
print(posted.status_code)
session.close()Do it immediately. A reCAPTCHA token is valid for about two minutes, so anything that sits between the solve and the post is spending that budget. The failure mode looks like a token that was rejected for no reason, and it is written up in full elsewhere if you hit it.
The request timeout defaults to 30 seconds in hrequests. That is fine here, because it covers the form post and not the solve. The solve has its own ceiling in the SDK: 120 seconds for image CAPTCHAs and 300 for reCAPTCHA, Turnstile and GeeTest.
Running the solver on another machine
The host argument above is the only thing that changes when the script and the solver stop sharing a machine. CapSkip has two connection modes. Local binds to 127.0.0.1 and serves that device only. Server binds to your network address or public IP, so a scraper on another box, a VPS or a container can reach the same Windows machine over the API. Both live under connection settings, and a static public IP is worth having if the caller is outside your network. Server mode changes only which address the solver listens on: same hardware, same machine, same unmetered solving.
import os
from capskip import CapSkip
# Same script on a laptop and on a scraping box. The env var
# decides; CAPSKIP_HOST and CAPSKIP_PORT are read by the SDK too.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
)Proxies are worth a word here, because hrequests and CapSkip take them separately. The proxy you give hrequests decides where your page fetch comes from. The proxy you give the solver decides where the challenge is solved from, and CapSkip accepts one for reCAPTCHA, Turnstile and GeeTest but not for image CAPTCHAs. Matching them matters on sites that bind a token to an address, and the reasoning is set out in the guide to CAPTCHA proxy rotation.
When you actually need the browser, and the trap in it
hrequests can hand a response to a real browser with a render call, and the appeal is that cookies come across in both directions: the browser session inherits the session’s cookies, and closing the page merges the new ones back. That is genuinely useful for a flow that has to click something.
Here is the part that costs people an afternoon. The Firefox engine in hrequests is Camoufox, launched directly from the Camoufox Python package, and Camoufox runs page scripts in an isolated scope. An isolated scope can read the DOM but cannot modify it, so the obvious move of evaluating a script that writes the token into the response textarea does nothing at all, silently. hrequests exposes a plain evaluate that takes a script and one argument, with no way to ask for the main world.
The way out is that hrequests forwards its extra keyword arguments straight to Camoufox, so Camoufox’s own switch is available to you. Turn on main world evaluation at launch, then prefix the script.
import hrequests
# The kwargs go through to Camoufox. Without main_world_eval the
# write below is discarded and nothing tells you.
page = hrequests.BrowserSession(headless=True, main_world_eval=True)
page.goto(SITE)
SCRIPT = (
"mw:(t) => { "
"document.getElementById('g-recaptcha-response').value = t; }"
)
# The second argument arrives as t inside the function.
page.evaluate(SCRIPT, token)
page.click("#submit")
page.close() # merges cookies back into the sessionTwo ways to avoid the whole question. Use the Chrome engine, which is ordinary Playwright semantics, at the cost of the fingerprint rotation and human cursor emulation that hrequests says only Firefox supports. Or do what Step 3 does and never inject at all: take the token back to the TLS session and post the form yourself. For a login or a search form that is both simpler and faster, and it is the reason this guide puts the browser last. If you are driving Camoufox directly rather than through hrequests, the isolated-scope behaviour and its other consequences are covered in the Camoufox CAPTCHA guide.
Solving several at once
hrequests gives you three ways to overlap requests, and CapSkip gives you one way to overlap solves. They compose, but they are not the same thing.
| What you want to overlap | Which tool does it |
|---|---|
| A handful of page fetches, fired and read later | Pass nohup set to true, then read an attribute when you need it |
| A list of URLs in one call | Hand the list straight to the request method |
| Many requests with a concurrency cap | Build unsent requests, then map them with a size limit |
| Several CAPTCHA solves at once | The Python SDK’s asynchronous client, which is a real implementation rather than an alias |
That last row is the one worth reading up on, because Python is the only CapSkip SDK where the async client is a separate implementation rather than a name for the same class. The batching pattern is written up in the guide to solving CAPTCHAs in parallel with Python. Choosing between hrequests and a mainstream async client for the fetching half is a separate question, and the httpx and aiohttp guide sets out the trade-offs.
Common errors and what they mean
| What you see | Cause | Fix |
|---|---|---|
| NetworkException from the SDK | CapSkip is not running, or not reachable from this machine | Start it, or switch to Server mode and set the host |
| The token is written but the widget stays unsolved | Camoufox’s isolated scope discarded the DOM write | Launch with main world evaluation on and prefix the script |
| MissingLibraryException on a render call | hrequests was installed without the browser extra | Install the extra, then run the library’s install command |
| A correct token is rejected by the site | The post went out on a different session or client | Post through the session that fetched the page |
| A correct token is rejected after a long gap | It expired before it was submitted | Solve and submit back to back, with nothing in between |
| ERROR_GOOGLEKEY in the response | The sitekey parsed out of the page is not the one in the widget | Read the data-sitekey attribute rather than a script tag |
| TimeoutException after 300 seconds | The sitekey and page URL are not the pair on that widget, so the solve runs out the 300 second ceiling | Check the sitekey and the page URL are the pair the widget actually uses |
| The browser session never releases | A page created without a context manager was not closed | Use the with form, or call close in a finally block |
FAQ
Does hrequests solve CAPTCHAs on its own?
No. It replicates browser TLS fingerprints and generates matching headers, which stops a lot of blocks before a challenge is ever drawn, and it can emulate human mouse movement and typing in a rendered page. None of that reads a distorted image or produces a reCAPTCHA token. Those are separate problems and they need a solver.
Do I need the browser extra at all?
Only if you have to render. The extra pulls in the browser stack and needs a separate install command afterwards, and it is a large download. For the common flow, which is fetch the page, read the sitekey, solve, post the form, the plain install is enough and the whole thing runs as HTTP requests. Add the extra when the widget genuinely is not in the served markup, or when the form only submits through real clicks.
Which browser should the session claim to be?
Whichever you name explicitly. hrequests accepts firefox or chrome and generates headers to match, and it deliberately does not keep the header version in step with the TLS version, on the grounds that detection systems rarely correlate the two and the extra spread looks like more clients. For rendering the library recommends Firefox, because Chrome there supports neither fingerprint rotation nor the human cursor emulation.
Can the solver run on a different machine from the scraper?
Yes, and this is the normal setup once scraping moves off a laptop. Put CapSkip in Server mode so it listens on your network address or a public IP instead of the loopback address, then point the host argument at it. A static public IP is recommended when the caller is outside your network. It is the same Windows machine doing the same unmetered solving; only the address changes.
The short version
Fetch with a named session so the fingerprint and the cookies are stable. Read the sitekey out of the response with the built in parser. Solve it with one SDK call. Post the token back through the same session, immediately, and only reach for a rendered browser when the widget is not in the markup. If you do render, remember that the Firefox engine is Camoufox and that a DOM write from an isolated scope disappears without an error, so turn main world evaluation on or skip the injection entirely.
- The Python SDK itself is covered on the Python CAPTCHA solver page.
- The checkbox challenge is covered on the reCAPTCHA v2 solver page.
One thing worth knowing before you scale a scraper that hits challenges often: CapSkip does captcha bypass on hardware you already own and never bills you per solve, so a run that hits ten challenges and a run that hits ten thousand cost exactly the same.
