Captcha solver in Node.js: working example with fetch (2026)
A captcha solver in Node.js is one HTTP request. Node 18 and later ship fetch, so you do not need a package to call the API. Send the sitekey and page URL, wait for the response, and put the token in the form. This guide shows the whole flow for PopularCaptcha, which Scarnox solves today on a proxyless basis.
Start with free trial credit, no card needed and run the code below against a test page first.
Requirements
- Node.js 18 or newer (
node --version). - A Scarnox API key. Keep it in an environment variable.
- The sitekey of the page's PopularCaptcha widget. If you do not have it yet, see how to find a captcha sitekey.
The solver module
The module below is a single function. It posts to the API with a Bearer header, aborts after 185 seconds, and throws when errorId is not 0:
// solver.mjs
const API_URL = "https://api.scarnox.com/api/tasks/create";
export async function solveCaptcha(sitekey, pageUrl, { invisible = false } = {}) {
const body = {
captcha_type: "PopularCaptchaTaskProxyless",
site_key: sitekey,
site_url: pageUrl,
};
if (invisible) body.is_invisible = true;
const res = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCARNOX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(185_000), // the call returns when the token is ready
});
const data = await res.json().catch(() => null);
if (!data) throw new Error(`HTTP ${res.status}: response was not JSON`);
if (data.errorId !== 0) {
throw new Error(`${data.errorCode}: ${data.errorDescription}`);
}
return data.solution.token;
}
There is no polling. The request stays open until the token is ready, and the token is in solution.token on success.
Retries
A failed solve returns errorId other than 0, and you are not charged for it. A short retry loop is enough for transient failures. Keep the number of attempts low, so a broken sitekey or page URL does not run up time:
export async function solveWithRetry(sitekey, pageUrl, attempts = 3) {
let lastError;
for (let i = 1; i <= attempts; i++) {
try {
return await solveCaptcha(sitekey, pageUrl);
} catch (err) {
lastError = err;
console.warn(`Attempt ${i} failed: ${err.message}`);
await new Promise((r) => setTimeout(r, 1000 * i));
}
}
throw lastError;
}
For a fuller error policy, see captcha solver error handling.
Submitting the token
The token goes into the form field the site expects. Most PopularCaptcha pages read h-captcha-response, and many also read g-recaptcha-response. Send both to be safe:
// submit.mjs
import { solveWithRetry } from "./solver.mjs";
const pageUrl = "https://example.com/signup";
const token = await solveWithRetry("YOUR-SITEKEY-UUID", pageUrl);
const form = new URLSearchParams({
email: "you@example.com",
"h-captcha-response": token,
"g-recaptcha-response": token,
});
const res = await fetch(pageUrl, {
method: "POST",
body: form,
redirect: "manual",
});
console.log("Status:", res.status);
Run it with:
SCARNOX_API_KEY=scarnox_YOURAPIKEY node submit.mjs
Cookies and sessions
Plain fetch does not keep cookies between requests. Many sign-up and search pages tie the captcha to a session cookie, so a token sent without the matching cookie can be rejected. There are two ways to handle it:
- Copy the
set-cookievalues from the GET response and send them back on the POST. - Use a browser automation tool, which keeps the session for you. See Playwright and Puppeteer captcha solving.
Pick the first when the form is simple and the site does not run much JavaScript. Pick the second for anything more involved.
A practical check: after the POST, look at the status code and the final URL. A redirect to a success page usually means the token was accepted. A page that shows the widget again usually means it was rejected, or that the cookie from the GET did not come along. Log the response body for the first few runs so you can see which case you are in.
Timing
The token expires in about two minutes. Solve close to the moment you submit. The usual mistake is to solve at the start of a long job and submit later. Solve, then submit immediately. Captcha token expiry covers this in more detail.
Invisible captchas
If the page's widget has data-size="invisible", pass { invisible: true }. The function then sends is_invisible: true with the request:
const token = await solveCaptcha("YOUR-SITEKEY-UUID", pageUrl, { invisible: true });
If the page also attaches a callback to the widget, the form may need that function called with the token. Read the page's script to find it. The invisible captcha guide covers the patterns.
Scraping with the solver
Sometimes the captcha shows up on a page you scrape rather than a form you submit. The same pattern works: detect the widget in the HTML, solve it, then submit it to the URL the widget posts to. Keep the request rate low and check the site's terms first. Scrape responsibly: respect robots.txt, avoid personal data you do not need, and do not hammer the server.
Pricing
Scarnox bills per successful solve. Failed solves are not charged. For current rates, see the pricing page. To start, start with free trial credit, no card needed.