The official NoneCap TypeScript SDK.
nonecap is the official TypeScript client for the
NoneCap hCaptcha solver API, and the shortest way to solve
hCaptcha in Node.js.
Submit a captcha, get back a token. The SDK handles the polling, the
timeouts, and the error cases so you don't write the fetch loop yourself.
Zero dependencies, and the types do real work.
Install
npm install nonecap
Runs on Node 18+, Bun, Deno, and edge runtimes. Get an API key from
dashboard.nonecap.com. Rather not
add a dependency? The plain fetch
section below drives the REST API directly.
Quick start
The headline is a single call. solve() submits the captcha and
waits until it's done, then returns the solved solve:
import { NoneCap } from "nonecap";
const nc = new NoneCap({ apiKey: process.env.NONECAP_KEY });
// Submit a captcha and wait for the token. One call, no polling loop.
const solve = await nc.solve({
type: "hcaptcha",
sitekey: "10000000-ffff-ffff-ffff-000000000001",
url: "https://example.com/login",
});
console.log(solve.token); // a real hCaptcha token, ready to submit Errors you can branch on
Every error this library throws extends NoneCapError, so you can
catch the whole family or pick out the one you care about. A failed solve
throws SolveFailedError with the full solve attached, so you get
the underlying error code and the timings:
import {
NoneCap,
SolveFailedError,
InsufficientCreditsError,
RateLimitError,
} from "nonecap";
try {
const { token } = await nc.solve({ type: "hcaptcha", sitekey, url });
// use token
} catch (err) {
if (err instanceof SolveFailedError) {
// What happened, what to do, and that nothing was charged.
console.error(err.solve.error?.message);
if (err.retryable) scheduleRetry();
} else if (err instanceof InsufficientCreditsError) {
console.error("Out of credits. Top up at dashboard.nonecap.com");
} else if (err instanceof RateLimitError) {
await sleep((err.retryAfter ?? 1) * 1000);
} else {
throw err;
}
} Enterprise sitekeys
Pass type: "hcaptcha_enterprise" for an enterprise sitekey.
rqdata is optional: many enterprise sites never issue one. When
the page does pass an rqdata blob to its widget, send it, fresh
for each challenge:
const { token } = await nc.solve({
type: "hcaptcha_enterprise",
sitekey,
url,
rqdata, // optional: the fresh challenge blob, when the site issues one
}); Zero dependencies: plain fetch
The API is ordinary REST and fetch has been global since Node 18, so
you can skip the package entirely. ?wait=90 makes the server hold the
connection until the solve reaches a terminal state, which means the token usually
comes back on the first response. The loop only matters for the occasional solve
that outlives one 90-second window:
// Node 18+: fetch is global, so this file has zero dependencies.
const API = "https://api.nonecap.com/v1";
const headers = {
Authorization: `Bearer ${process.env.NONECAP_KEY}`,
"Content-Type": "application/json",
};
async function solveHcaptcha(sitekey, url) {
// wait=90 holds the connection until the solve is terminal,
// so the token usually comes back on the first response.
let res = await fetch(`${API}/solves?wait=90`, {
method: "POST",
headers,
body: JSON.stringify({ type: "hcaptcha", sitekey, url }),
});
let solve = await res.json();
if (!res.ok) {
// 401 bad key, 402 out of credits, 422 bad params, 429 over
// your concurrency cap. The body says which.
throw new Error(`HTTP ${res.status}: ${JSON.stringify(solve)}`);
}
// A 202 means the solve is still in flight (pending or solving):
// keep re-attaching with GET until it reaches a terminal state.
while (solve.status === "pending" || solve.status === "solving") {
res = await fetch(`${API}/solves/${solve.id}?wait=90`, { headers });
solve = await res.json();
}
if (solve.status !== "solved") {
throw new Error(`solve ${solve.status}: ${solve.error?.code}`);
}
return solve.token; // a real P1_... token
}
const token = await solveHcaptcha(
"f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8",
"https://example.com/login",
); Full request and response shapes, including the error body format, are in the API reference.
Long jobs: submit now, collect later
Holding a connection open is the simple default, but it ties up the caller. If
you're queueing solves from a job worker, or you just don't want a request hanging
for tens of seconds, create the solve without ?wait and collect the
result separately:
// Submit without wait= and the API returns the pending solve
// immediately. Poll it on your own schedule.
let res = await fetch(`${API}/solves`, {
method: "POST",
headers,
body: JSON.stringify({ type: "hcaptcha", sitekey, url }),
});
let solve = await res.json(); // { id: "solve_01HQF...", status: "pending", ... }
// Later, from anywhere that has the id:
res = await fetch(`${API}/solves/${solve.id}?wait=30`, { headers });
solve = await res.json(); You have a token. Now what?
The token goes into the form field named h-captcha-response. In a
pure-HTTP pipeline that's one extra field in the POST you were already sending:
// Pure-HTTP flow: put the token in the form POST you were
// going to send anyway. g-recaptcha-response is hCaptcha's
// compatibility field name, same token in both.
await fetch("https://target.example/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
email,
password,
"h-captcha-response": token,
"g-recaptcha-response": token,
}),
});
Driving a browser instead? Set the value of
textarea[name="h-captcha-response"] on the page, plus the
g-recaptcha-response compatibility field if it exists, then fire the
widget callback for invisible sitekeys and submit. The
Puppeteer guide and the
Playwright guide cover the injection details
for each driver. And if you want the mechanics of what's inside a token, why it's
single-use, and why it expires after roughly two minutes, that's the
hCaptcha token deep dive.
One rule either way: don't hard-code the sitekey. Sites rotate them, and enterprise
deployments bind challenges to the page. Read it fresh from the live page, either
the [data-sitekey] attribute on the widget or the sitekey
query param on the hcaptcha.com/1/api.js script tag.
Lower-level methods
solve() is the convenient path. When you want control over
submission and polling, the resource methods map one to one to the REST API:
// Submit without blocking, then poll the one solve yourself.
const pending = await nc.solves.create({ type: "hcaptcha", sitekey, url });
const done = await nc.solves.retrieve(pending.id, { wait: 30 });
// Cancel, list, or iterate every solve newest-first.
await nc.solves.cancel(pending.id);
for await (const s of nc.solves.listAll()) console.log(s.id, s.status);
// Your account and credit balance.
const me = await nc.me();
console.log(me.credits_balance);
// Tell us when your target rejects a token — free, and it tunes minting
// against your real acceptance rate. Reporting accepted ones is optional.
// submitToTarget is your own form post; true when the target took the token.
const targetAccepted = await submitToTarget(done.token);
if (!targetAccepted) {
await nc.feedback.report({ solve_id: done.id, outcome: "rejected" });
} Python
There's an official Python client too:
pip install nonecap.
Same shape as the TypeScript SDK, with sync and async clients, the typed
error tree, and full inline types for mypy and pyright. Source at
github.com/nonecap/nonecap-py,
full guide at Python hCaptcha solver.
# pip install nonecap
from nonecap import NoneCap
nc = NoneCap(api_key="nc_live_...")
solve = nc.solve(
type="hcaptcha",
sitekey="10000000-ffff-ffff-ffff-000000000001",
url="https://example.com/login",
)
print(solve.token) # a real hCaptcha token, ready to submit
# Async? Same surface: AsyncNoneCap, awaited. Not using JavaScript or Python?
The API is a plain REST endpoint, so any language works. The
API reference has runnable examples in cURL,
Node, Python, and Go, the Go hCaptcha solver
guide builds a complete client on net/http, and the integration guides
cover Playwright, Puppeteer, web scraping, and AI agents. Prefer a no-code
option? The Chrome extension solves hCaptcha right
in your browser.
When this isn't the right approach
NoneCap solves hCaptcha only: regular, invisible, and enterprise
rqdatasitekeys. If the page is actually protected by reCAPTCHA, Cloudflare Turnstile, or FunCaptcha, this guide doesn't apply; those are on the roadmap, not live. And if your target has no captcha at all, skip the token step and POST the form directly.
Last updated October 2026.
Frequently asked
What runtimes does it work on?
fetch and ships zero dependencies. The package is dual ESM and CommonJS with bundled type definitions.Does it handle polling and timeouts for me?
nc.solve() submits the captcha and uses the API's long-poll under the hood, keeping the connection open until the solve finishes or your timeout elapses. It returns the solved solve, or throws a typed error you can branch on.How are errors typed?
NoneCapError, so you can catch the whole family or a specific one: AuthenticationError, InsufficientCreditsError, ValidationError (with the offending param), RateLimitError, NotFoundError, ConflictError, and more. A failed solve throws SolveFailedError with the full solve attached.How much does each solve cost?
How many solves can I run in parallel?
Promise.all over a big array will blow past the cap and the extra requests come back 429 (the SDK throws RateLimitError), so run the batch through a small pool, for example p-limit sized at or under your cap, and retry anything that got rate-limited.Do I need a headless browser in my Node app?
fetch pipeline can include the token as h-captcha-response in the form POST it was already sending. You only need Puppeteer or Playwright when the rest of the job needs a page; the Puppeteer guide and Playwright guide cover injecting the token there.