nodriver and zendriver: setup, proxies and hCaptcha.

nodriver is an async Python library that drives Chrome without Selenium or ChromeDriver. How to install it, a quick start, headless mode and proxies, how its fork zendriver differs, and what to do when a site shows hCaptcha.

What nodriver is

nodriver is written by UltrafunkAmsterdam, who also wrote undetected-chromedriver. Its PyPI page says, in bold: “This is the official successor of the Undetected-Chromedriver python package.” and “No more webdriver, no more selenium”. Where undetected-chromedriver patches the ChromeDriver that Selenium uses, nodriver leaves both out: it starts the browser itself and sends commands over the Chrome DevTools Protocol (CDP), the same channel Chrome’s developer tools use.

nodriver and zendriver at a glance (read from PyPI and GitHub on 2 November 2026)
PackageMaintainerLatest releaseLicence and Python
nodriverUltrafunkAmsterdam0.50.99, 26 October 2026AGPL-3.0, Python 3.9+
zendriver, a fork of nodriverStephan Lensky, cdpdriver on GitHub0.17.1, 2 October 2026AGPL-3.0, Python 3.10+

The README lists Chromium, Chrome, Edge and Brave as “known to work with”, and sums up its aim as “the optimization to stay undetected for most anti-bot solutions”. Those are the project’s claims; we did not test them against any anti-bot service.

How it differs from Selenium and undetected-chromedriver

  • No WebDriver. There is no ChromeDriver to download or keep in step with your Chrome version, so the version-mismatch error that undetected-chromedriver users know does not come up.
  • Async. You await nearly every call inside an async function. A Selenium script has to be rewritten, not just re-imported.
  • A fresh profile per run. By default nodriver starts Chrome with a new temporary profile and deletes it on exit. Pass user_data_dir to keep one.
  • Its own element lookup. tab.select() takes a CSS selector and tab.find() searches by visible text; both wait for the element to appear. Only find() and select_all(include_frames=True) look inside iframes: in our run select() returned None for an input inside one.

Selenium-Driverless also drives Chrome over CDP without ChromeDriver, but keeps Selenium’s method names; it is now out of maintenance.

Install nodriver

Shell · pip
pip install nodriver

It needs Python 3.9 or newer, and it does not bring a browser: install Chrome or another Chromium browser first, “preferably in the default location”, as the README puts it, or point to it with browser_executable_path. Its only dependencies are websockets, mss and deprecated.

One thing to know when you check which version you have: nodriver.__version__ in the 0.50.99 package still says 0.50.8. Use pip show nodriver instead.

Quick start in Python

Python · nodriver 0.50.99
import nodriver as uc


async def main():
    browser = await uc.start(headless=True)
    tab = await browser.get("https://your-site.example/")
    print(await tab.evaluate("navigator.webdriver"))
    print(await tab.evaluate("document.title"))
    browser.stop()


if __name__ == "__main__":
    uc.loop().run_until_complete(main())

We ran this on 2 November 2026 with nodriver 0.50.99, Python 3.13 and Chrome 156, headless. navigator.webdriver read False. The README starts its examples with uc.loop().run_until_complete(main()), noting that “asyncio.run never worked (for me)”; in our run, asyncio.run(main()) worked as well. browser.stop() is a plain method in nodriver, so it is not awaited.

Finding and filling elements looks like this:

Python · find, select, type
button = await tab.find("accept all")
await button.click()

field = await tab.select("input[name=username]")
await field.send_keys("my-username")

find() matches by text and, by default, prefers the element whose text is closest in length, so “accept all” lands on the cookie button rather than a script that contains the words.

Headless mode

uc.start(headless=True) runs Chrome’s headless mode. The README’s first choice on a server with no screen is a virtual display such as Xvfb; headless is the alternative. In our run, headless nodriver kept navigator.webdriver false, but both navigator.userAgent and the User-Agent header said HeadlessChrome/156.0.0.0. Passing a normal user agent yourself removed it from both:

Python · headless user agent
ua = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36"
)

browser = await uc.start(headless=True, browser_args=[f"--user-agent={ua}"])

Use a proxy with nodriver

To send the whole browser through one proxy, give Chrome its own flag:

Python · proxy for the browser
browser = await uc.start(
    browser_args=["--proxy-server=http://203.0.113.10:8080"],
)

Chrome’s flag has no place for a username and password. For a proxy that needs a login, nodriver has browser.create_context(), which opens a new browser context (its own cookies and storage) with its own proxy. For any proxy you pass there, nodriver starts a small proxy on your machine that forwards to it and adds the login when the URL has one:

Python · proxy with a login
browser = await uc.start()
tab = await browser.create_context(
    "https://your-site.example/",
    proxy_server="http://user:pass@host:port",
)

We tested both against local test proxies. With the flag, every request went through the proxy. With create_context, an https page loaded through a proxy that required a login, and the login was sent. A plain http:// page did not load through that context: nodriver’s local proxy only passes on https traffic. The docstring marks proxy_server as “(EXPERIMENTAL)”.

Is nodriver still maintained?

Yes. Version 0.50.99 went to PyPI on 26 October 2026, the fourth release that day, after 0.50.7, 0.50.8 and 0.50.9. The project has had 50 releases since the first one in February 2024, and the author wrote 149 of its commits. Its GitHub tracker takes issues and pull requests; 22 were open on 2 November 2026. PyPI still classifies it as “Development Status :: 3 - Alpha”, and the README asks you to “test thoroughly, especially if you run large projects” after the 0.50 rewrite of how it connects to pages.

zendriver, the nodriver fork

zendriver is maintained by Stephan Lensky under the cdpdriver organisation on GitHub. Its README says it “is a fork of ultrafunkamsterdam/nodriver, created to add new features, compile unmerged bugfixes, and increase community engagement”, and gives the reason: “contributions to the original nodriver repo are heavily restricted, making it difficult to submit issues or pull requests.” That was written when the fork started; nodriver’s own tracker took new issues and pull requests in October 2026. zendriver’s latest release is 0.17.1, from 2 October 2026, and 61 issues and pull requests were open on 2 November 2026. Its docs are at zendriver.dev.

Shell · pip
pip install zendriver
Python · zendriver 0.17.1
import asyncio

import zendriver as zd


async def main():
    browser = await zd.start(headless=True)
    page = await browser.get("https://your-site.example/")
    print(await page.evaluate("navigator.userAgent"))
    await browser.stop()


if __name__ == "__main__":
    asyncio.run(main())

The API is close to nodriver’s: start(), browser.get(), select(), find() and evaluate() work the same way. What we saw differ when we ran both on 2 November 2026:

Differences we ran into (Python 3.13, Chrome 156, headless)
nodriver 0.50.99zendriver 0.17.1
Run it withuc.loop().run_until_complete(main()) in the README; asyncio.run worked tooasyncio.run(main())
Close the browserbrowser.stop(), not awaitedawait browser.stop()
Headless user agentSays HeadlessChrome unless you pass --user-agentDrops “Headless” from it by itself
Your own user agentbrowser_args=["--user-agent=..."]zd.start(user_agent="...")
Issues and pull requests22 open61 open

zendriver’s create_context() also takes a proxy with a login; in our test it loaded an https page through one. Its start() also takes browser="chrome", "msedge" or "brave" to choose which installed browser to launch.

Another Python library that drives Chrome over CDP without WebDriver is Pydoll, which has both a sync and an async API; our Pydoll guide covers its setup, proxies and hCaptcha.

What nodriver does when a site shows hCaptcha

The nodriver repository describes itself as a framework for “web automation, webscraping, bots and any other creative ideas which are normally hindered by annoying anti bot systems like Captcha / CloudFlare / Imperva / hCaptcha”. Neither library has any hCaptcha code, though. The one challenge helper in each, tab.verify_cf(), looks for Cloudflare’s checkbox and clicks it; the nodriver README lists it as cf_verify() and says it needs opencv-python and works with English pages only. The README also notes that since 0.50.1 find() searches inside iframes, so you can “click the verification checkbox in js challenges”. Clicking hCaptcha’s checkbox only hands the decision to hCaptcha, which may answer with a puzzle.

A browser that shows fewer automation signals is one of several things that decide whether hCaptcha challenges you at all; why am I getting hCaptcha covers the others, starting with the reputation of your IP address. Once hCaptcha is on the page, the site accepts the form only with a valid token in the h-captcha-response field, a P1_ string its server checks with hCaptcha. Invisible hCaptcha needs that token too. nodriver and zendriver do not produce one.

Get an hCaptcha token into a nodriver session

NoneCap is an hCaptcha solving API. Keep nodriver for the rest of the run, and when a page has hCaptcha, send its sitekey and url to NoneCap, get a P1_ token back, put it in the form and submit. The Python SDK (pip install nonecap) has an async client, AsyncNoneCap, that fits nodriver’s event loop, and solve() waits for the result:

Python · get an hCaptcha token
import os

import nodriver as uc
from nonecap import AsyncNoneCap


async def main():
    browser = await uc.start()
    tab = await browser.get("https://your-site.example/signup")
    widget = await tab.select("[data-sitekey]")

    async with AsyncNoneCap(api_key=os.environ["NONECAP_KEY"]) as nc:
        solve = await nc.solve(
            type="hcaptcha",
            sitekey=widget.attrs["data-sitekey"],
            url=await tab.evaluate("location.href"),
            user_agent=await tab.evaluate("navigator.userAgent"),
        )

    await fill_and_submit(tab, solve.token)
    browser.stop()

Then put the token in both response fields, call the widget’s callback if the page set one with data-callback, and submit:

Python · fill the token, submit
import json

FILL = """(() => {
    const token = %s;
    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);
})()"""


async def fill_and_submit(tab, token):
    await tab.evaluate(FILL % json.dumps(token))
    button = await tab.select("button[type=submit]")
    await button.click()


if __name__ == "__main__":
    uc.loop().run_until_complete(main())

We tested the fill-and-submit step against a local test page with an hCaptcha-style form and a fake token: both fields held the token, the page’s callback received it, and the form posted it as h-captcha-response and g-recaptcha-response. The same fill-and-submit step ran in zendriver with import zendriver as zd, await browser.stop() and asyncio.run.

Sending the browser’s own user_agent is optional; some sites need it for the token to be accepted. If the browser goes through a proxy, you can pass the same one to NoneCap as proxy (http://user:pass@host:port) so the token and the form come from the same IP address. Failed, cancelled and expired solves are never charged.

Every field, status and error is in the API reference. NoneCap solves hCaptcha only; it does not help with reCAPTCHA, Turnstile or other CAPTCHAs.

Solve hCaptcha in Python →

Last updated November 2026.

Frequently asked

What is nodriver?
A Python library for automating Chrome and other Chromium browsers. It talks to the browser directly over the Chrome DevTools Protocol, with no Selenium and no ChromeDriver, and its API is async. It is written by UltrafunkAmsterdam, the author of undetected-chromedriver, and licensed AGPL-3.0. Install it with pip install nodriver.
Is nodriver the successor of undetected-chromedriver?
Yes, by its author’s own description: its PyPI page says “This is the official successor of the Undetected-Chromedriver python package.” It is not a drop-in replacement. undetected-chromedriver is a patched Selenium driver with a synchronous API; nodriver drops Selenium and WebDriver and is async, so moving over means rewriting the script.
Is nodriver still maintained?
Yes. The latest release, 0.50.99, came out on 26 October 2026, and the GitHub repository had commits that same day. PyPI still lists it as “Development Status :: 3 - Alpha”, and the README asks you to “test thoroughly” after the 0.50 rewrite.
What is zendriver, and should I use it or nodriver?
zendriver is a fork of nodriver by Stephan Lensky, “created to add new features, compile unmerged bugfixes, and increase community engagement”. The API is close to nodriver’s. zendriver runs under asyncio.run, awaits browser.stop(), takes a user_agent argument and hides “Headless” in the user agent by itself. Pick the one whose releases and issue tracker suit you; both were updated in October 2026.
Does nodriver work in headless mode?
Yes: uc.start(headless=True). The README prefers a virtual display such as Xvfb on servers and calls headless the alternative. In our run, headless nodriver kept navigator.webdriver false but sent HeadlessChrome in its user agent; passing your own --user-agent in browser_args removed it.
How do I use a proxy with nodriver?
For the whole browser, pass Chrome’s flag: browser_args=["--proxy-server=http://host:port"]. For a proxy with a username and password, open a new context with browser.create_context(url, proxy_server="http://user:pass@host:port"); nodriver runs a small local proxy that adds the login. In our test that loaded an https page through a proxy that requires a login.
Does nodriver bypass hCaptcha?
It does not solve it. The repository description names hCaptcha among the “annoying anti bot systems” that normally get in the way, but neither nodriver nor zendriver has any hCaptcha code; their one challenge helper, verify_cf, clicks Cloudflare’s checkbox. When a site shows hCaptcha, the form still needs a valid token in h-captcha-response, and neither library produces one. NoneCap returns that token.
Is it allowed to use nodriver and a solver on a site?
That depends on the site and on what you do there. Automate where you are allowed to: your own sites, your tests, and services whose terms permit it. NoneCap’s terms rule out fraud, credential stuffing, spam and unauthorised access. This is not legal advice.

Start solving hCaptcha in minutes.

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