Integrations

Puppeteer Stealth + CaptchaAI: Reliable Browser Automation

Standard Puppeteer gets detected immediately by anti-bot systems. puppeteer-extra-plugin-stealth patches the telltale signs — navigator properties, WebGL, Chrome runtime flags — that reveal automation. Combined with CaptchaAI for solving any CAPTCHA that still appears, you get a powerful stack for reliable browser automation.

This guide covers stealth configuration, detection handling techniques, and CaptchaAI integration for reCAPTCHA, Turnstile, and challenge pages.


Why Stealth Matters for CAPTCHA Automation

Standard Puppeteer exposes ~20 detectable signals:

Signal Detection Stealth Fix
navigator.webdriver true in puppeteer Set to undefined
Chrome runtime Missing chrome.runtime Adds mock object
Permissions API Reveals automation Patches responses
WebGL vendor Reports SwiftShader Spoofs real GPU
Plugin count 0 in headless Injects mock plugins
iframe access Blocked cross-origin Fixed handling

Without stealth patches, anti-bot systems serve harder CAPTCHAs or block requests entirely.


Setup

npm install puppeteer-extra puppeteer-extra-plugin-stealth puppeteer

Basic Stealth + CaptchaAI Integration

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

class StealthCaptchaSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://ocr.captchaai.com';
  }

  async launch(options = {}) {
    this.browser = await puppeteer.launch({
      headless: 'new',
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-blink-features=AutomationControlled',
        '--window-size=1920,1080',
      ],
      ...options,
    });
    return this.browser;
  }

  async solveCaptchaOnPage(page) {
    // Auto-detect CAPTCHA type
    const captchaInfo = await this.detectCaptcha(page);
    if (!captchaInfo) return null;

    console.log(`Detected ${captchaInfo.type} CAPTCHA`);

    const token = await this.solve(captchaInfo);

    // Inject token into page
    await this.injectToken(page, captchaInfo.type, token);

    return token;
  }

  async detectCaptcha(page) {
    return page.evaluate(() => {
      // reCAPTCHA v2
      const recaptchaV2 = document.querySelector('[data-sitekey]');
      if (recaptchaV2) {
        return {
          type: 'recaptcha_v2',
          sitekey: recaptchaV2.getAttribute('data-sitekey'),
          pageUrl: window.location.href,
        };
      }

      // reCAPTCHA v3 (in script)
      const scripts = document.querySelectorAll('script[src*="recaptcha"]');
      for (const script of scripts) {
        const src = script.src;
        const renderMatch = src.match(/render=([A-Za-z0-9_-]+)/);
        if (renderMatch && renderMatch[1] !== 'explicit') {
          return {
            type: 'recaptcha_v3',
            sitekey: renderMatch[1],
            pageUrl: window.location.href,
          };
        }
      }

      // Turnstile
      const turnstile = document.querySelector('.cf-turnstile[data-sitekey]');
      if (turnstile) {
        return {
          type: 'turnstile',
          sitekey: turnstile.getAttribute('data-sitekey'),
          pageUrl: window.location.href,
        };
      }

      return null;
    });
  }

  async solve(captchaInfo) {
    const params = { key: this.apiKey, json: 1 };

    switch (captchaInfo.type) {
      case 'recaptcha_v2':
        params.method = 'userrecaptcha';
        params.googlekey = captchaInfo.sitekey;
        params.pageurl = captchaInfo.pageUrl;
        break;
      case 'recaptcha_v3':
        params.method = 'userrecaptcha';
        params.googlekey = captchaInfo.sitekey;
        params.pageurl = captchaInfo.pageUrl;
        params.version = 'v3';
        params.action = 'verify';
        params.min_score = '0.7';
        break;
      case 'turnstile':
        params.method = 'turnstile';
        params.key = captchaInfo.sitekey;
        params.pageurl = captchaInfo.pageUrl;
        break;
    }

    // Submit
    const submitResp = await fetch(`${this.baseUrl}/in.php`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams(params),
    });
    const submitData = await submitResp.json();
    if (submitData.status !== 1) throw new Error(`Submit: ${submitData.request}`);

    // Poll
    const taskId = submitData.request;
    for (let i = 0; i < 60; i++) {
      await new Promise(r => setTimeout(r, 5000));

      const pollResp = await fetch(
        `${this.baseUrl}/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const pollData = await pollResp.json();

      if (pollData.request === 'CAPCHA_NOT_READY') continue;
      if (pollData.status !== 1) throw new Error(`Solve: ${pollData.request}`);
      return pollData.request;
    }

    throw new Error('Timeout');
  }

  async injectToken(page, type, token) {
    switch (type) {
      case 'recaptcha_v2':
      case 'recaptcha_v3':
        await page.evaluate((t) => {
          document.querySelector('#g-recaptcha-response').value = t;
          // Also set in iframe textarea if present
          const iframes = document.querySelectorAll('iframe[src*="recaptcha"]');
          iframes.forEach(iframe => {
            try {
              const textarea = iframe.contentDocument?.querySelector('#g-recaptcha-response');
              if (textarea) textarea.value = t;
            } catch (e) {}
          });
          // Trigger callback
          if (typeof window.___grecaptcha_cfg !== 'undefined') {
            const clients = Object.values(window.___grecaptcha_cfg.clients || {});
            for (const client of clients) {
              const callback = Object.values(client).find(
                v => typeof v === 'object' && v?.callback
              );
              if (callback?.callback) callback.callback(t);
            }
          }
        }, token);
        break;

      case 'turnstile':
        await page.evaluate((t) => {
          const input = document.querySelector('[name="cf-turnstile-response"]');
          if (input) input.value = t;
          // Trigger turnstile callback
          if (window.turnstile) {
            const widgets = document.querySelectorAll('.cf-turnstile');
            widgets.forEach(w => {
              const callback = w.getAttribute('data-callback');
              if (callback && window[callback]) window[callback](t);
            });
          }
        }, token);
        break;
    }
  }

  async close() {
    if (this.browser) await this.browser.close();
  }
}

Complete Workflow Example

async function automateProtectedSite() {
  const solver = new StealthCaptchaSolver('YOUR_API_KEY');

  try {
    await solver.launch();
    const page = await solver.browser.newPage();

    // Set realistic viewport and user agent
    await page.setViewport({ width: 1920, height: 1080 });

    // Navigate to target
    await page.goto('https://example.com/login', { waitUntil: 'networkidle2' });

    // Fill form fields
    await page.type('#email', 'user@example.com', { delay: 50 });
    await page.type('#password', 'password123', { delay: 50 });

    // Solve CAPTCHA
    const token = await solver.solveCaptchaOnPage(page);
    console.log(`CAPTCHA solved: ${token?.substring(0, 50)}...`);

    // Submit form
    await page.click('#login-button');
    await page.waitForNavigation({ waitUntil: 'networkidle2' });

    console.log(`Current URL: ${page.url()}`);

  } finally {
    await solver.close();
  }
}

automateProtectedSite().catch(console.error);

Advanced Stealth Configuration

Selective Plugin Evasions

const StealthPlugin = require('puppeteer-extra-plugin-stealth');

const stealth = StealthPlugin();

// Enable/disable specific evasions
stealth.enabledEvasions.delete('chrome.runtime');
stealth.enabledEvasions.add('navigator.permissions');

puppeteer.use(stealth);

Custom Browser Fingerprint

async function setupFingerprint(page) {
  // Override WebGL renderer
  await page.evaluateOnNewDocument(() => {
    const getParameter = WebGLRenderingContext.prototype.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
      if (parameter === 37445) return 'Intel Inc.';
      if (parameter === 37446) return 'Intel Iris OpenGL Engine';
      return getParameter.call(this, parameter);
    };
  });

  // Set realistic timezone
  await page.emulateTimezone('America/New_York');

  // Set navigator properties
  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
    Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
    Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
  });
}

Handling Cloudflare Challenge Pages

async function handleCloudflareChallenge(page) {
  // Wait for challenge to appear
  const isChallenged = await page.evaluate(() => {
    return document.title.includes('Just a moment') ||
           document.querySelector('#challenge-running') !== null;
  });

  if (!isChallenged) return;

  console.log('Cloudflare challenge detected');

  // Check for Turnstile widget
  await page.waitForSelector('.cf-turnstile', { timeout: 10000 }).catch(() => null);

  const turnstileKey = await page.evaluate(() => {
    const el = document.querySelector('.cf-turnstile[data-sitekey]');
    return el?.getAttribute('data-sitekey');
  });

  if (turnstileKey) {
    const solver = new StealthCaptchaSolver('YOUR_API_KEY');
    const token = await solver.solve({
      type: 'turnstile',
      sitekey: turnstileKey,
      pageUrl: page.url(),
    });

    await page.evaluate((t) => {
      const input = document.querySelector('[name="cf-turnstile-response"]');
      if (input) {
        input.value = t;
        input.dispatchEvent(new Event('change', { bubbles: true }));
      }
    }, token);

    await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 });
  }
}

Request Interception for CAPTCHA Detection

async function interceptCaptchaRequests(page) {
  const captchaRequests = [];

  await page.setRequestInterception(true);

  page.on('request', (request) => {
    const url = request.url();

    // Log CAPTCHA-related requests
    if (url.includes('recaptcha') || url.includes('turnstile') || url.includes('hcaptcha')) {
      captchaRequests.push({
        url,
        type: url.includes('recaptcha') ? 'recaptcha' :
              url.includes('turnstile') ? 'turnstile' : 'hcaptcha',
        timestamp: Date.now(),
      });
    }

    // Block unnecessary resources for speed
    const blocked = ['image', 'stylesheet', 'font'];
    if (blocked.includes(request.resourceType()) && !url.includes('captcha')) {
      request.abort();
    } else {
      request.continue();
    }
  });

  return captchaRequests;
}

Multi-Page Session Management

async function multiPageWorkflow(apiKey) {
  const solver = new StealthCaptchaSolver(apiKey);
  await solver.launch();

  const page = await solver.browser.newPage();

  // Step 1: Login page
  await page.goto('https://example.com/login', { waitUntil: 'networkidle2' });
  await page.type('#email', 'user@example.com', { delay: 30 });
  await page.type('#password', 'password', { delay: 30 });
  await solver.solveCaptchaOnPage(page);
  await page.click('#submit');
  await page.waitForNavigation();

  // Step 2: Navigate to protected area
  await page.goto('https://example.com/dashboard', { waitUntil: 'networkidle2' });

  // Step 3: Trigger another CAPTCHA
  await page.click('#export-data');
  await page.waitForSelector('[data-sitekey]', { timeout: 5000 }).catch(() => null);
  await solver.solveCaptchaOnPage(page);

  // Collect cookies for future requests
  const cookies = await page.cookies();
  console.log(`Session cookies: ${cookies.length}`);

  await solver.close();
  return cookies;
}

Detection Testing

Verify your stealth setup works:

async function testStealth() {
  const solver = new StealthCaptchaSolver('YOUR_API_KEY');
  await solver.launch();
  const page = await solver.browser.newPage();

  // Test against popular detection sites
  await page.goto('https://bot.sannysoft.com/');
  await page.screenshot({ path: 'stealth-test.png', fullPage: true });

  // Check specific values
  const results = await page.evaluate(() => ({
    webdriver: navigator.webdriver,
    languages: navigator.languages,
    plugins: navigator.plugins.length,
    platform: navigator.platform,
    hardwareConcurrency: navigator.hardwareConcurrency,
  }));

  console.log('Detection results:', results);
  await solver.close();
}

Troubleshooting

Issue Cause Fix
Still detected as bot Missing stealth evasions Update puppeteer-extra-plugin-stealth to latest
CAPTCHA not found Dynamic loading Add waitForSelector before detection
Token injection fails Shadow DOM or iframe Use page.frames() to access CAPTCHA frame
Browser crashes Memory leak Close pages after use, limit concurrent tabs
Slow performance Blocking resources Use request interception to block images/CSS
ERR_CERT_AUTHORITY_INVALID Self-signed cert Add --ignore-certificate-errors arg

FAQ

Does stealth mode guarantee no CAPTCHAs?

No. Stealth reduces the chance of being served CAPTCHAs, but some sites always show them. CaptchaAI handles the ones that appear.

Should I use headless or headed mode?

Headless 'new' mode with stealth plugin passes most detection. Use headed mode only for debugging.

How often should I rotate user agents?

Rotate per session, not per page. Changing user agent mid-session is a detection signal itself.

Can I use this with Puppeteer Cluster?

Yes. puppeteer-extra works with puppeteer-cluster for parallel browser instances.



Build stealth-optimized browser automation — get your CaptchaAI API key and integrate with Puppeteer Stealth.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.