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-sitekey and the page url.
  • 2. Solve by calling POST /v1/solves (block with ?wait=N, or go async with a webhook_url).
  • 3. Inject the returned token into textarea[name="h-captcha-response"] via page.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:

Python · Playwright
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:

Node · Playwright
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.

hCaptcha sitekey types and the matching NoneCap request
Sitekey typeHow you spot itWhat to send NoneCap
CheckboxA visible "I am human" widget renders; a textarea[name="h-captcha-response"] appears in the formtype: "hcaptcha" + sitekey + url
InvisibleNo widget; hcaptcha.execute() runs on an action and a data-callback handles the tokentype: "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 · rqdata
# 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?
Stealth patches the obvious automation tells, but modern hCaptcha scores the whole session. That means TLS and browser fingerprint, input cadence, behavioural signals, and on enterprise sitekeys each challenge is bound to a fresh 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?
The 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?
Invisible widgets do not submit on a value change; they submit from the 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.
How much does each solve cost?
At least one credit per solve. Billing is one credit per hCaptcha challenge round (often one, sometimes two or three), charged only on success, and failed solves are auto-refunded. Credits run $0.40-$0.50 per 1,000 depending on your pack, you get 100 free credits on signup, and there is no subscription. See pricing.

Start solving hCaptcha in minutes.

100 free credits on signup. Pay per solve, credits never expire, failed solves auto-refunded.