Developer Guides

CAPTCHA Solver for Puppeteer — Node.js Integration Guide

Puppeteer is the most widely used Node.js browser automation library. When your Puppeteer script hits a CAPTCHA, you need to pause the browser session, solve the CAPTCHA via an API, inject the token, and continue. This guide covers the complete integration pattern.

For a multi-type Node.js overview, see the Node.js Quick Start Guide. For the equivalent Playwright guide, see CAPTCHA Solver for Playwright.

Prerequisites

npm install puppeteer axios

Core Solver Utility (shared across all types)

// captcha-solver.js
const axios = require("axios");

const CAPTCHAI_KEY = process.env.CAPTCHAI_KEY || "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const POLL_URL = "https://ocr.captchaai.com/res.php";

async function pollForResult(taskId, { initialDelay = 5000, interval = 5000, maxAttempts = 24 } = {}) {
  await new Promise(r => setTimeout(r, initialDelay));

  for (let i = 0; i < maxAttempts; i++) {
    const { data } = await axios.get(POLL_URL, {
      params: { key: CAPTCHAI_KEY, action: "get", id: taskId, json: 1 },
      timeout: 30000,
    });

    if (data.status === 1) return data.request;
    if (!["CAPCHA_NOT_READY", "CAPTCHA_NOT_READY"].includes(data.request)) {
      throw new Error(`Solver error: ${data.request}`);
    }
    await new Promise(r => setTimeout(r, interval));
  }
  throw new Error(`CAPTCHA solve timed out (task: ${taskId})`);
}

async function solveRecaptchaV2(pageUrl, siteKey) {
  const { data } = await axios.post(SUBMIT_URL, new URLSearchParams({
    key: CAPTCHAI_KEY, method: "userrecaptcha",
    googlekey: siteKey, pageurl: pageUrl, json: 1,
  }), { timeout: 30000 });

  if (data.status !== 1) throw new Error(`Submit failed: ${data.request}`);
  return pollForResult(data.request, { initialDelay: 5000 });
}

async function solveRecaptchaV3(pageUrl, siteKey, action = "verify", minScore = 0.7) {
  const { data } = await axios.post(SUBMIT_URL, new URLSearchParams({
    key: CAPTCHAI_KEY, method: "userrecaptcha", version: "v3",
    googlekey: siteKey, pageurl: pageUrl, action, min_score: minScore, json: 1,
  }), { timeout: 30000 });

  if (data.status !== 1) throw new Error(`Submit failed: ${data.request}`);
  return pollForResult(data.request, { initialDelay: 8000 });
}

async function solveHCaptcha(pageUrl, siteKey) {
  const { data } = await axios.post(SUBMIT_URL, new URLSearchParams({
    key: CAPTCHAI_KEY, method: "hcaptcha",
    sitekey: siteKey, pageurl: pageUrl, json: 1,
  }), { timeout: 30000 });

  if (data.status !== 1) throw new Error(`Submit failed: ${data.request}`);
  return pollForResult(data.request, { initialDelay: 10000 });
}

async function solveTurnstile(pageUrl, siteKey) {
  const { data } = await axios.post(SUBMIT_URL, new URLSearchParams({
    key: CAPTCHAI_KEY, method: "turnstile",
    sitekey: siteKey, pageurl: pageUrl, json: 1,
  }), { timeout: 30000 });

  if (data.status !== 1) throw new Error(`Submit failed: ${data.request}`);
  return pollForResult(data.request, { initialDelay: 5000 });
}

module.exports = { solveRecaptchaV2, solveRecaptchaV3, solveHCaptcha, solveTurnstile };

Solving reCAPTCHA v2 in Puppeteer

const puppeteer = require("puppeteer");
const { solveRecaptchaV2 } = require("./captcha-solver");

async function loginWithRecaptcha(url, username, password) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto(url, { waitUntil: "networkidle2" });

  // Extract site key from the live page
  const siteKey = await page.$eval("[data-sitekey]", el => el.dataset.sitekey).catch(() => null);

  if (siteKey) {
    const token = await solveRecaptchaV2(url, siteKey);

    // Inject into all g-recaptcha-response fields
    await page.evaluate((t) => {
      document.querySelectorAll('[name="g-recaptcha-response"]').forEach(el => {
        el.value = t;
        el.dispatchEvent(new Event("change", { bubbles: true }));
      });
    }, token);
  }

  await page.type('[name="username"], #username', username);
  await page.type('[name="password"], #password', password);
  await page.click('button[type="submit"], input[type="submit"]');
  await page.waitForNavigation({ waitUntil: "networkidle2" });

  console.log("Post-login URL:", page.url());
  await browser.close();
}

Solving reCAPTCHA v3 in Puppeteer

v3 requires extracting the action name from the page JS:

const { solveRecaptchaV3 } = require("./captcha-solver");

async function submitV3Form(url) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: "networkidle2" });

  // Extract sitekey and action
  const siteKey = await page.$eval("[data-sitekey]", el => el.dataset.sitekey).catch(() => null);
  const pageContent = await page.content();
  const actionMatch = pageContent.match(/action\s*:\s*["']([a-zA-Z0-9_/]+)["']/);
  const action = actionMatch ? actionMatch[1] : "verify";

  const token = await solveRecaptchaV3(url, siteKey, action, 0.7);

  await page.evaluate((t) => {
    document.querySelectorAll('[name="g-recaptcha-response"]').forEach(el => { el.value = t; });
    // Override grecaptcha.execute to return pre-solved token
    if (window.grecaptcha?.execute) {
      window.grecaptcha.execute = () => Promise.resolve(t);
    }
  }, token);

  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: "networkidle2" });
  await browser.close();
}

Solving hCaptcha in Puppeteer

const { solveHCaptcha } = require("./captcha-solver");

async function submitHCaptchaForm(url) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: "networkidle2" });

  const siteKey = await page.$eval("[data-sitekey]", el => el.dataset.sitekey).catch(() => null);
  const token = await solveHCaptcha(url, siteKey);

  await page.evaluate((t) => {
    // hCaptcha uses textarea, not input
    const textarea = document.querySelector('[name="h-captcha-response"]');
    if (textarea) textarea.value = t;

    // Some sites also populate g-recaptcha-response for compatibility
    const gcr = document.querySelector('[name="g-recaptcha-response"]');
    if (gcr) gcr.value = t;
  }, token);

  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: "networkidle2" });
  await browser.close();
}

Solving Cloudflare Turnstile in Puppeteer

const { solveTurnstile } = require("./captcha-solver");

async function submitTurnstileForm(url) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: "networkidle2" });

  const siteKey = await page.$eval("[data-sitekey]", el => el.dataset.sitekey).catch(() => null);
  const token = await solveTurnstile(url, siteKey);

  await page.evaluate((t) => {
    const field = document.querySelector('[name="cf-turnstile-response"]');
    if (field) field.value = t;
    if (window.turnstile?.getResponse) {
      window.turnstile.getResponse = () => t;
    }
  }, token);

  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: "networkidle2" });
  await browser.close();
}

Stealth Configuration

Puppeteer is easily fingerprinted as a bot. Use puppeteer-extra with the stealth plugin to reduce detection:

npm install puppeteer-extra puppeteer-extra-plugin-stealth
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
puppeteer.use(StealthPlugin());

// Then use puppeteer exactly as before — stealth patches apply automatically
const browser = await puppeteer.launch({ headless: true });

The stealth plugin addresses the most common bot detection signals: navigator.webdriver, missing Chrome APIs, headless User-Agent strings, and CDP fingerprinting.

Error Handling and Retry

async function solveWithRetry(solveFn, maxRetries = 3) {
  let lastError;
  for (let i = 1; i <= maxRetries; i++) {
    try {
      return await solveFn();
    } catch (err) {
      lastError = err;
      console.warn(`Attempt ${i}/${maxRetries} failed: ${err.message}`);
      await new Promise(r => setTimeout(r, 3000 * i));
    }
  }
  throw new Error(`All ${maxRetries} retries failed. Last: ${lastError.message}`);
}

// Usage
const token = await solveWithRetry(() => solveRecaptchaV2(url, siteKey));

Puppeteer vs Playwright for CAPTCHA Solving

Both work well with solver APIs. Key differences:

Aspect Puppeteer Playwright
Language Node.js only Node.js + Python + .NET
Stealth support Via plugin (puppeteer-extra-plugin-stealth) Via playwright-stealth
Network interception page.on('request', ...) page.route(...) — more ergonomic
Parallel contexts Manual Built-in BrowserContext

For Python automation, use Playwright. For Node.js automation without a strong preference, either works — see CAPTCHA Solver for Playwright for the Playwright equivalent.

Production Readiness Notes

Use CAPTCHA Solver for Puppeteer — Node.js Integration Guide as a decision and implementation aid, not just as a one-time reference. The practical test for captcha solver for puppeteer is whether the same approach behaves reliably when traffic is messy: rotating sessions, expired tokens, changing widget parameters, intermittent solver delays, and target pages that refresh without warning. For Node.js developer building browser automation with Puppeteer, the safest rollout is to start with a narrow fixture, record every submitted task, and compare the solver response with the browser state that finally submits the form. That makes failures explainable instead of mysterious, especially when a target alternates between visible challenges, invisible checks, and server-side verification.

Evaluation Criteria

A how-to should be exercised against staging first, then promoted with feature flags so failed solves can fall back without blocking the entire workflow. For developer integration work, the most useful scorecard combines technical acceptance with operational cost. A low nominal price is not enough if retries double the real cost per accepted token, and a fast median solve time is not enough if p95 latency stalls the queue. Track these criteria before you standardize the workflow:

  • The challenge subtype, sitekey, action, rqdata, blob, captchaId, or page URL used for each task.
  • Median and p95 solve time, separated by provider and target domain.
  • Accepted-token rate on the target page, not just successful API responses.
  • Retry count, timeout count, zero-balance incidents, and invalid-parameter errors.
  • The exact browser, proxy region, and user-agent that submitted the solved token.

Rollout Checklist

Before this guidance moves into a production job, build a small acceptance suite around the pages that matter most. Run it with a fixed browser profile, then repeat with the proxy and concurrency settings you expect in production. Keep the first release conservative: bounded polling, clear timeout handling, and a fallback path when the solver cannot return a usable answer. For developer integration, treat the code as a production pattern: timeouts, retries, logging, secret storage, and test fixtures matter as much as the solve request itself. That checklist keeps the article useful after the first copy-paste, because the integration is judged by end-to-end completion rather than by whether a code sample returned a string.

Monitoring Signals

Healthy CAPTCHA automation is observable. Log the task id, provider, challenge type, target host, queue time, solve time, final submit status, and normalized error code for every attempt. Review those logs in daily batches at first, then move to alerts once the baseline is stable. Sudden drops usually come from target-side changes: a new sitekey, a changed action name, a stricter hostname check, an added managed challenge, or a proxy pool that no longer matches the expected geography. When you can see those shifts quickly, provider switching becomes a controlled decision instead of a late-night rewrite.

Maintenance Cadence

Revisit the setup whenever the target UI changes, when the solver provider changes task names or pricing, or when benchmark data shows a sustained latency or solve-rate shift. Keep one known-good fixture for each CAPTCHA subtype and rerun it after dependency upgrades, browser updates, and proxy changes. If the article is used for vendor selection, repeat the same fixture across at least two providers before renewing a balance or migrating the whole pipeline. That habit keeps captcha solver for puppeteer work aligned with the real target behavior rather than with stale assumptions.

Comments are disabled for this article.