Solve hCaptcha in Playwright (Python & Node).
Don't try to beat the challenge inside your automated browser. Read the
data-sitekey off the page, ask NoneCap for a real token, inject it
into the form's h-captcha-response field, then submit. It's the same four
steps in Python and in Node.
Why vanilla stealth usually isn't enough
A headless Playwright session with a stealth plugin gets you past the crudest
automation checks, but it doesn't get you a valid hCaptcha token. Modern hCaptcha
scores the whole session: the TLS and browser fingerprint, the
pointer and timing behaviour, the reputation of the IP. It only hands out a token
when that score looks human. Enterprise sitekeys go further and bind every challenge to a
fresh rqdata blob tied to your request. A patched automation browser
is exactly the profile these systems are built to deny.
So instead of solving the challenge inside the browser you're automating,
you solve it out of band. NoneCap solves it separately, off your
machine, then returns a normal P1_ token. Your Playwright script never
touches the challenge UI. It just supplies the token the page was already waiting for.
The four steps
Every integration, regardless of language, is the same shape:
- 1. Read the
data-sitekeyand the pageurl. - 2. Solve by calling
POST /v1/solves(block with?wait=N, or go async with awebhook_url). - 3. Inject the returned token into
textarea[name="h-captcha-response"]viapage.evaluate, and fire the widget callback if there is one. - 4. Submit the form the way your page does.
Python (Playwright)
Using the sync API and requests for the NoneCap call. ?wait=90
blocks the request until the solve reaches a terminal state, so you get the
token back inline:
import os
import requests
from playwright.sync_api import sync_playwright
NONECAP_KEY = os.environ["NONECAP_KEY"]
def solve_with_nonecap(sitekey: str, url: str) -> str:
r = requests.post(
"https://api.nonecap.com/v1/solves",
headers={"Authorization": f"Bearer {NONECAP_KEY}"},
params={"wait": 90}, # block up to 90s for the token
json={"type": "hcaptcha", "sitekey": sitekey, "url": url},
timeout=120,
)
r.raise_for_status()
data = r.json()
if data["status"] != "solved":
raise RuntimeError(f"solve {data['status']}: {data.get('error')}")
return data["token"] # a real P1_… token
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://target.example/login")
# 1. Read the sitekey straight off the rendered widget.
sitekey = page.locator("[data-sitekey]").first.get_attribute("data-sitekey")
# 2. Mint a real token out-of-band with NoneCap.
token = solve_with_nonecap(sitekey, page.url)
# 3. Inject the hCaptcha token into h-captcha-response. hCaptcha's drop-in
# mode reuses reCAPTCHA's old g-recaptcha-response field name, so we set
# it too if present, it's still the hCaptcha token, not a reCAPTCHA solve.
page.evaluate(
"""(token) => {
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.
page.click("button[type=submit]")
page.wait_for_load_state("networkidle")
browser.close() Node (Playwright)
The same flow in JavaScript with fetch and the Playwright Node client:
import { chromium } from "playwright";
const NONECAP_KEY = process.env.NONECAP_KEY;
async function solveWithNoneCap(sitekey, url) {
const res = await fetch("https://api.nonecap.com/v1/solves?wait=90", {
method: "POST",
headers: {
Authorization: `Bearer ${NONECAP_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ type: "hcaptcha", sitekey, url }),
});
const data = await res.json();
if (data.status !== "solved") {
throw new Error(`solve ${data.status}: ${JSON.stringify(data.error)}`);
}
return data.token; // a real P1_… token
}
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto("https://target.example/login");
// 1. Read the sitekey straight off the rendered widget.
const sitekey = await page
.locator("[data-sitekey]")
.first()
.getAttribute("data-sitekey");
// 2. Mint a real token out-of-band with NoneCap.
const token = await solveWithNoneCap(sitekey, page.url());
// 3. Inject the hCaptcha token into h-captcha-response. hCaptcha's drop-in
// mode reuses reCAPTCHA's old g-recaptcha-response field name, so we set it
// too if present, it's still the hCaptcha token, not a reCAPTCHA solve.
await page.evaluate((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);
}, token);
// 4. Submit the form however your page does it.
await page.click("button[type=submit]");
await page.waitForLoadState("networkidle");
await browser.close(); 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.
| Sitekey type | How you spot it | What to send NoneCap |
|---|---|---|
| Checkbox | A visible "I am human" widget renders; a textarea[name="h-captcha-response"] appears in the form | type: "hcaptcha" + sitekey + url |
| Invisible | No widget; hcaptcha.execute() runs on an action and a data-callback handles the token | type: "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),
which the examples above already do.
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 sitekeys also need the per-challenge rqdata blob.
# Read it from the call the page makes (hcaptcha.render / execute),
# then forward it with the enterprise type:
requests.post(
"https://api.nonecap.com/v1/solves",
headers={"Authorization": f"Bearer {NONECAP_KEY}"},
params={"wait": 90},
json={
"type": "hcaptcha_enterprise",
"sitekey": sitekey,
"url": page.url,
"rqdata": rqdata, # the fresh, IP-bound challenge blob
},
) Async instead of blocking
The examples block with ?wait=90, which is the simplest path for a
single solve. For high-throughput crawls you can omit ?wait 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, up to 50 on paid plans), so a fleet of Playwright 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 Playwright and skip the token step entirely.
Driving Puppeteer or
Selenium instead of Playwright? The flow is
identical. Read the data-sitekey, mint the token, set
h-captcha-response in the DOM, and fire the callback. For the wider
pipeline, see hCaptcha for web scraping.
Last updated June 2026.
Frequently asked
Why not just solve hCaptcha inside Playwright with a stealth plugin?
rqdata blob. A patched headless Playwright still looks like a patched headless Playwright. NoneCap solves it separately, off your machine, and hands back a finished token, so your Playwright run only has to inject and submit.Where do I get the sitekey and url to send?
sitekey is on the rendered widget. Read it in Playwright with page.locator("[data-sitekey]").get_attribute("data-sitekey") (or scrape it from the hcaptcha.render() call). The url is just the page the challenge appears on, i.e. page.url. Send both to POST /v1/solves.The widget is invisible and injecting the token does nothing. What now?
data-callback function. After setting textarea[name="h-captcha-response"], look up the callback name from the data-callback attribute and call window[name](token), exactly as the example does. If the form posts through your own handler instead, just click submit after injecting.