Solve hCaptcha in Node.js.
Two ways to do it, both shown in full below. The official nonecap npm
package turns the whole flow into one awaited call, and if you'd rather not add a
dependency, the global fetch in Node 18+ can drive the REST API in
about thirty lines. Either way you end up with a real hCaptcha token, and signup
comes with 100 free credits to test with.
Where the solving actually happens
Not in your Node process. You send NoneCap a sitekey and a page URL; NoneCap solves
the challenge on its own servers and returns a normal P1_ token as a
string. There's no headless browser in your stack for hCaptcha to fingerprint and
no challenge iframe for you to automate. Your code has two jobs: read the sitekey
off the target page, and put the returned token where the form expects it.
Fastest path: the npm package
npm install nonecap Runs on Node 18+, Bun, Deno, and edge runtimes, with zero dependencies and bundled TypeScript definitions. Grab an API key from dashboard.nonecap.com and the headline is a single call:
import { NoneCap } from "nonecap";
const nc = new NoneCap({ apiKey: process.env.NONECAP_KEY });
// One call. solve() submits the captcha and long-polls
// until it's done, then returns the solved solve.
const solve = await nc.solve({
type: "hcaptcha",
sitekey: "f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8",
url: "https://example.com/login",
});
console.log(solve.token); // a real hCaptcha token, ready to submit nc.solve() submits the captcha and long-polls until the solve is
terminal: the server holds each request open for up to 90 seconds and the client
keeps re-attaching until the token arrives or its own timeout runs out. You don't
write the polling loop. The error plumbing is done for you too: everything
the SDK throws extends NoneCapError, with specific classes you can
branch on with instanceof: SolveFailedError arrives with
the full solve attached (error code, timings), and RateLimitError,
InsufficientCreditsError, ValidationError, and
AuthenticationError cover the rest.
The types do real work too. The solve parameters are a discriminated union on
type, so an enterprise solve that forgets rqdata fails at
compile time instead of in production. Lower-level resource methods
(nc.solves.create, retrieve, cancel,
listAll, nc.me()) map one to one onto the REST API. The
full surface lives on the SDK page, so this guide won't repeat
it.
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.
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.
Pairing the solve with a browser? Start with hCaptcha in Puppeteer or hCaptcha in Playwright. Running a no-browser pipeline at scale, read the web scraping guide; building autonomous tools, the AI agents guide. And if your project is Python rather than Node, the same client exists there: pip install nonecap.
Last updated June 2026.
Frequently asked
How much does each solve cost?
Does it support enterprise sitekeys with rqdata?
type: "hcaptcha_enterprise" plus the rqdata blob you captured from the live page, and the returned token is one the enterprise sitekey accepts. In the npm SDK the parameter type is a discriminated union on type, so forgetting rqdata on an enterprise solve is a compile error. Background on what the blob is and how to capture it: enterprise rqdata explained.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.What runtimes does the npm package support?
fetch, ships zero dependencies, and is dual ESM and CommonJS with bundled type definitions. MIT licensed, source at github.com/nonecap/nonecap-js.