nodriver and Cloudflare Turnstile
nodriver is the official successor to undetected-chromedriver — no webdriver, no Selenium, fully asynchronous, talking to Chrome directly over the DevTools Protocol. Its README documents a tab.cf_verify() that "finds the checkbox and click it successfully". That method is not in the shipped package: the only occurrence of the name anywhere in the wheel is the README text embedded in the metadata.
What nodriver is
Its README states the lineage and the design in four lines: "This is the official successor of the Undetected-Chromedriver python package. No more webdriver, no more selenium." And on why: "Direct communication provides even better resistance against web applicatinon firewalls (WAF's), while performance gets a massive boost. This module is, contrary to undetected-chromedriver, fully asynchronous."
That is a real architectural change rather than a rewrite for its own sake. Removing the webdriver layer removes the whole class of tells that undetected-chromedriver existed to patch out — there is no chromedriver binary injecting cdc_ variables because there is no chromedriver.
import nodriver as uc async def main(): browser = await uc.start() page = await browser.get("https://www.nowsecure.nl") if __name__ == "__main__": # since asyncio.run never worked (for me) uc.loop().run_until_complete(main())
The core API is small: browser.get(url) returns a tab; tab.find(text), tab.select(css), tab.xpath(…) locate elements; tab.evaluate(expression) runs JavaScript; elements expose send_keys, set_value, clear_input, click and apply.
The documented method that does not exist
The README carries this section:
"tab.cf_verify() — finds the checkbox and click it successfully. this only works when NOT in expert mode. currently built-in english only. requires opencv-python package to be installed"
It is the reason a lot of people install nodriver. Checked against the published package, the name appears in exactly one place: the README text embedded in the wheel's METADATA. There are zero occurrences in any Python file — not in tab.py, not anywhere in the source tree on the main branch, and not in the older release checked either. A search of the repository finds it only in the README and docs.
So tab.cf_verify() will raise AttributeError if you call it. Not a bug in your code — the method is documented and unshipped.
The neighbouring README note is worth reading anyway, because it tells you something true about the design: "only works when NOT in expert mode." And expert mode carries its own warning: "start(expert=True) does some hacking for more experienced users. It disables web security and origin-trials, as well as ensures shadow-roots are always open. This makes you more detectable though!"
That trade-off is the real story for Turnstile. Forcing shadow roots open is what makes the widget's internals reachable by ordinary selectors — and it is also what makes the browser more detectable. The thing that lets you click the checkbox is the thing that gets you noticed.
What does work for reaching the widget
Later releases rewrote parts of the protocol handling to use flat connections, and the README describes the consequence: "iframes are included in most operations… find() will include iframes, so you can even search for 'verify you are human' and click the verification checkbox in js challenges."
That is the supported route, and it is a text search rather than a selector, which is deliberate — the widget's internal markup is not stable but its visible text is:
import nodriver as uc async def main(): browser = await uc.start() page = await browser.get("https://app.example.com/login") # find() descends into iframes, which is what makes this reachable at all el = await page.find("verify you are human", best_match=True, timeout=15) if el: await el.click() uc.loop().run_until_complete(main())
Whether the click is accepted is a separate question from whether it lands, and Cloudflare's position on the category is unambiguous: automated browsers are not supported for solving production challenges. Clicking is not the hard part; being the kind of client whose click counts is.
One deployment note from the README: "when running on a headless machine, like AWS or any other environment where no display is present, it's best to use some Xvfb tool, to emulate a screen." Same advice as every other tool here, and the same reason.
Licensing, which matters more than usual here
nodriver is AGPL-3.0. That is a stronger copyleft than the MIT and Apache licences most of this ecosystem uses, and the network clause is the part that catches people: running AGPL code as part of a service you offer over a network can oblige you to offer that service's source. If nodriver is going into a commercial product, this is a question for whoever answers licensing questions, not an afterthought.
By contrast, undetected-chromedriver is GPL-3.0 and Patchright is Apache-2.0. Patchright is worth comparing on that axis alone.
Fetching the token instead
Because nodriver has no working checkbox helper and clicking is the fragile part anyway, the shape that survives contact is the same as everywhere else: get a token, write it into the field, submit.
import json, os, urllib.request import nodriver as uc SITEKEY = "0x4AAAAAAA_target" PAGE = "https://app.example.com/login" def fetch_token(): req = urllib.request.Request( "https://api.solvegate.io/v1/solve", data=json.dumps({"gate": "turnstile", "sitekey": SITEKEY, "url": PAGE}).encode(), headers={ "Authorization": f"Bearer {os.environ['SOLVEGATE_KEY']}", "Content-Type": "application/json", }, ) with urllib.request.urlopen(req, timeout=30) as res: return json.load(res)["token"] async def main(): token = fetch_token() browser = await uc.start() page = await browser.get(PAGE) await page.evaluate( f''' const el = document.querySelector('[name="cf-turnstile-response"]'); el.value = {json.dumps(token)}; el.dispatchEvent(new Event('input', {{ bubbles: true }})); ''' ) uc.loop().run_until_complete(main())
The field name is cf-turnstile-response unless the page renamed it — our checker reports the real one. Tokens are single-use and expire in minutes, so fetch at the moment you submit.
A token typically returns in under 1.5 seconds. Prepaid credits run from $0.40 per 1,000 down to $0.075 at volume; failed solves are never billed; the first 1,000 are free.
Common questions
Because it is not in the package. The name appears only in the README, including the copy of the README embedded in the wheel's metadata — there are no occurrences in any Python file in the shipped release or on the main branch. It is documented and unshipped.
It is the official successor and architecturally cleaner — no webdriver layer, so none of the chromedriver tells that the older package existed to patch out. Neither ships a working Turnstile solver, and neither addresses IP reputation, which is often the actual reason a server deployment fails.
Use find() with the visible text rather than a selector. Later releases route most operations through flat connections so find() descends into iframes, and the README specifically mentions searching for "verify you are human" to reach the checkbox in JS challenges. Whether the click is accepted is a separate question from whether it lands.
It disables web security and origin-trials and forces shadow roots open, which makes the widget's internals reachable by ordinary selectors. The README also says it makes you more detectable. That is the trade-off in one sentence: the thing that lets you reach inside the widget is the thing that marks the browser as unusual.
It is worth checking. nodriver is AGPL-3.0, whose network clause can oblige you to offer source for a service built on it — a stronger obligation than the MIT and Apache licences most of this ecosystem uses. Get an answer from whoever answers licensing questions before it is load-bearing.
Related
Sources
More in Guides
Automating a gate you own or are authorised to test?
// SolveGate clears Cloudflare Turnstile and WAF challenges through one REST call · first 1,000 solves free