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

Terminal
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:

Node · TypeScript
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 · fetch, no SDK
// 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:

Node · submit then poll
// 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:

Node · submit the token
// 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 rqdata sitekeys. 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?
At least one credit per solve. Billing starts at one credit per hCaptcha challenge round (hCaptcha decides how many: often one, sometimes two or three), charged only on success, so failed solves are never charged. Credits run $0.25 to $0.50 per 1,000 depending on your pack, you get 100 free credits on signup, and there is no subscription. See pricing.
Does it support enterprise sitekeys with rqdata?
Yes. Send 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?
Concurrency is capped per account: 5 solves in flight on the free trial, up to 100 on paid packs. 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?
No. The token is just a string. NoneCap solves the challenge on its own servers, so a pure 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?
Node 18 and newer, plus Bun, Deno, and edge runtimes. It uses the built-in fetch, ships zero dependencies, and is dual ESM and CommonJS with bundled type definitions. MIT licensed, source at github.com/nonecap/nonecap-js.

Start solving hCaptcha in minutes.

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