How to Solve reCAPTCHA v2 in Node.js, Including Invisible

solve recaptcha v2 in node.js - How to Solve reCAPTCHA v2 in Node.js, Including Invisible

reCAPTCHA v2 has three variants and they all resolve to the same Node.js method. Checkbox is the bare call, Invisible and Enterprise are options in a third argument, and the two combine. The package also ships its own TypeScript definitions, so none of this needs a separate @types install.

Setup

npm install capskip

CapSkip solves on your own machine, so the desktop app has to be running first. Then point the client at the port from its settings:

const { CapSkip } = require('capskip');

const solver = new CapSkip({
  apiKey: 'capskip',        // any string when key validation is off
  host: '127.0.0.1',
  port: 8080,
  recaptchaTimeout: 300,    // seconds
});

In production, read the connection details from the environment:

const solver = new CapSkip({
  apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
  host: process.env.CAPSKIP_HOST || '127.0.0.1',
  port: parseInt(process.env.CAPSKIP_PORT || '8080', 10),
});

The three variants

VariantOption to add
Checkboxnone
Invisible{ invisible: 1 }
Enterprise{ enterprise: 1 }
Invisible Enterpriseboth keys
// Checkbox: sitekey and page URL, nothing else.
const result = await solver.recaptcha(
  '6Lc...YOUR_SITEKEY',
  'https://example.com/login',
);

console.log(result.code);   // g-recaptcha-response token

// Invisible.
await solver.recaptcha(sitekey, pageUrl, { invisible: 1 });

// Enterprise, and both at once.
await solver.recaptcha(sitekey, pageUrl, { enterprise: 1 });
await solver.recaptcha(sitekey, pageUrl, { enterprise: 1, invisible: 1 });

Note the argument style. Node takes the sitekey and URL positionally, unlike the Python SDK which uses keywords. Porting code between the two is where most mistakes creep in.

TypeScript

Type definitions ship with the package, so this works with no extra install:

import { CapSkip, SolveResult } from 'capskip';

const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const result: SolveResult = await solver.recaptcha(sitekey, pageUrl);

SolveResult carries captchaId and code for every type, userAgent for Turnstile, and the three GeeTest fields. For reCAPTCHA only code matters.

Getting the sitekey right

It is the data-sitekey attribute on the widget container, or the first argument to grecaptcha.render when Invisible mode means there is no visible container. It always starts with 6L and is public.

The URL has to be the page the widget renders on. Passing a form handler or a post-login redirect is the usual cause of a token that solves cleanly and then fails verification.

Submitting the token

const body = new URLSearchParams({
  'g-recaptcha-response': result.code,
  username: '...',
  password: '...',
});

const response = await fetch('https://example.com/login', {
  method: 'POST',
  body,
});

Tokens are single use and last around two minutes, so solve as late as possible. If the site passes the token to a JavaScript callback rather than a form field, the solve is the same but submission differs, which our reCAPTCHA v2 callback solver page covers.

Solving several at once

const [a, b] = await Promise.all([
  solver.recaptcha(sitekeyA, 'https://a.example.com'),
  solver.recaptcha(sitekeyB, 'https://b.example.com'),
]);

console.log(a.code, b.code);

The package also exports AsyncCapSkip, but in Node it is only an alias of CapSkip. Node I/O is already asynchronous and every method already returns a Promise, so it exists purely so code ported from the Python SDK keeps working. Switching to it gains nothing.

Errors

const {
  ValidationException, NetworkException, ApiException, TimeoutException,
} = require('capskip');

try {
  const result = await solver.recaptcha(sitekey, pageUrl);
} catch (err) {
  if (err instanceof ValidationException)      { /* bad arguments */ }
  else if (err instanceof NetworkException)    { /* CapSkip not running */ }
  else if (err instanceof ApiException)        { /* bad sitekey or url */ }
  else if (err instanceof TimeoutException)    { /* polling timed out */ }
  else throw err;
}

All four extend a shared base, so catching CapSkipError handles everything in one branch if you prefer.

Frequently asked questions

Do I need to poll for the result?

No. The client polls internally and the Promise resolves with the finished token. It starts checking after 250ms and backs off toward pollingInterval, which usually beats a hand-written loop.

Does it work with ESM and import syntax?

Yes. The examples here use require for brevity, but import { CapSkip } from 'capskip' works, and the bundled TypeScript definitions come along with it.

Can I use this alongside Puppeteer or Playwright?

Yes. Solve the token with this SDK, then inject it into the page with page.evaluate before submitting. Our Node.js CAPTCHA solver page covers the browser automation side.

Summary

One method, three variants, selected with invisible and enterprise. Arguments are positional, the token lands in result.code, and Promise.all is all you need for concurrency because AsyncCapSkip is just an alias here.

The wider Node surface is on the Node.js CAPTCHA solver page, other languages on the reCAPTCHA v2 solver page, and there is a live v2 demo to test against. CapSkip is a local captcha solver, so nothing is billed per solve.