Tutorials

CAPTCHA Handling in Progressive Web Apps (PWAs)

Progressive Web Apps present unique CAPTCHA challenges. They use client-side rendering, Service Workers, and single-page navigation — meaning CAPTCHAs load dynamically rather than with the initial HTML. CaptchaAI handles the solving, but you need the right detection strategy to catch CAPTCHAs that are injected into the DOM after the page loads.

This guide covers detecting, extracting, and solving CAPTCHAs in PWA contexts with Playwright and CaptchaAI.

Why PWAs Are Different

Traditional websites serve CAPTCHAs in the initial HTML response. PWAs differ in several key ways:

Aspect Traditional Site PWA
CAPTCHA loading In initial HTML Rendered by JavaScript after page load
Page navigation Full page reload Client-side routing (no reload)
Service Worker Not present Caches resources, may intercept requests
DOM availability Immediate After framework renders
Network requests Direct May be intercepted by Service Worker

Step 1: Wait for Dynamic CAPTCHA Rendering

The biggest mistake is trying to extract sitekeys before the PWA framework has rendered the CAPTCHA widget. Use mutation observers or framework-specific signals:

// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function detectCaptchaInPWA(page) {
  // Wait for the PWA app shell to render
  await page.waitForLoadState('networkidle');

  // Use MutationObserver to detect dynamically loaded CAPTCHAs
  const captchaInfo = await page.evaluate(() => {
    return new Promise((resolve) => {
      // Check if CAPTCHA is already present
      const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
      if (existing) {
        resolve({
          type: existing.classList.contains('g-recaptcha')
            ? 'recaptcha_v2' : 'turnstile',
          sitekey: existing.getAttribute('data-sitekey'),
          pageurl: window.location.href,
        });
        return;
      }

      // Watch for CAPTCHA elements added dynamically
      const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node.nodeType !== 1) continue;
            const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
              ? node
              : node.querySelector?.('.g-recaptcha, .cf-turnstile');
            if (captcha) {
              observer.disconnect();
              resolve({
                type: captcha.classList.contains('g-recaptcha')
                  ? 'recaptcha_v2' : 'turnstile',
                sitekey: captcha.getAttribute('data-sitekey'),
                pageurl: window.location.href,
              });
              return;
            }
          }
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      // Timeout after 15 seconds
      setTimeout(() => {
        observer.disconnect();
        resolve(null);
      }, 15000);
    });
  });

  return captchaInfo;
}

async function main() {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://example-pwa.com/login');

  const captcha = await detectCaptchaInPWA(page);

  if (!captcha) {
    console.log('No CAPTCHA detected');
    await browser.close();
    return;
  }

  console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);

  // Solve with CaptchaAI
  const token = await solveCaptcha(captcha);
  console.log(`Token: ${token.substring(0, 50)}...`);

  // Inject token
  await injectToken(page, captcha.type, token);

  // Submit form
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle' });

  console.log('Form submitted');
  await browser.close();
}

async function solveCaptcha(captcha) {
  const params = {
    key: API_KEY,
    pageurl: captcha.pageurl,
    json: '1',
  };

  if (captcha.type === 'recaptcha_v2') {
    params.method = 'userrecaptcha';
    params.googlekey = captcha.sitekey;
  } else {
    params.method = 'turnstile';
    params.sitekey = captcha.sitekey;
  }

  const submit = await axios.get(
    'https://ocr.captchaai.com/in.php', { params }
  );
  if (submit.data.status !== 1) throw new Error(submit.data.request);

  const taskId = submit.data.request;

  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      throw new Error(poll.data.request);
    }
  }
  throw new Error('Timeout');
}

async function injectToken(page, type, token) {
  if (type === 'recaptcha_v2') {
    await page.evaluate((t) => {
      document.getElementById('g-recaptcha-response').value = t;
      try {
        const clients = ___grecaptcha_cfg.clients;
        Object.keys(clients).forEach((k) => {
          Object.keys(clients[k]).forEach((j) => {
            if (clients[k][j]?.callback) clients[k][j].callback(t);
          });
        });
      } catch (e) {}
    }, token);
  } else {
    await page.evaluate((t) => {
      const input = document.querySelector('[name="cf-turnstile-response"]');
      if (input) input.value = t;
      const cb = document.querySelector('.cf-turnstile')
        ?.getAttribute('data-callback');
      if (cb && typeof window[cb] === 'function') window[cb](t);
    }, token);
  }
}

main().catch(console.error);

Step 2: Handle Service Worker Caching

Service Workers can cache CAPTCHA scripts, leading to stale widgets. Bypass the cache when needed:

// Intercept and bypass Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

await page.route('**/turnstile/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

Step 3: Handle Client-Side Navigation

PWAs use client-side routing — navigating to a CAPTCHA-protected route doesn't trigger a page load. Monitor route changes:

// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
  const originalPushState = history.pushState;
  history.pushState = function() {
    originalPushState.apply(this, arguments);
    window.dispatchEvent(new Event('pwa-route-change'));
  };
});

page.on('console', async (msg) => {
  // React to route changes if needed
});

// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);

Step 4: Python Alternative with Selenium

# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

API_KEY = "YOUR_API_KEY"

driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")

# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)

sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")

# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
    params["method"] = "turnstile"
    params["sitekey"] = sitekey
else:
    params["method"] = "userrecaptcha"
    params["googlekey"] = sitekey

resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]

# Poll
for _ in range(30):
    time.sleep(5)
    poll = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1",
    })
    if poll.json().get("status") == 1:
        token = poll.json()["request"]
        break
else:
    raise TimeoutError("CAPTCHA not solved")

# Inject token
driver.execute_script(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()

Troubleshooting

Problem Cause Fix
CAPTCHA element never appears PWA hasn't rendered the route yet Use waitForSelector with extended timeout; ensure client-side routing completed
Stale sitekey after navigation Service Worker served cached HTML Bypass cache headers for CAPTCHA-related resources
Token injection callback not found PWA framework manages state differently Check for React/Vue/Angular state management; trigger form state update
Form submission doesn't send token SPA form handler reads from component state, not DOM Also update the framework's state (e.g., React ref, Vue reactive property)

FAQ

Do PWAs use different CAPTCHA types than regular sites?

No. PWAs use the same reCAPTCHA, Turnstile, and other CAPTCHA widgets. The difference is timing — they're loaded dynamically.

Can Service Workers block CAPTCHA solving?

Service Workers can cache CAPTCHA scripts, but you can bypass this with cache-busting headers or disabling the Service Worker during automation.

Does CaptchaAI handle PWA-specific tokens differently?

No. The tokens are identical whether the CAPTCHA was served in a PWA or a traditional page. CaptchaAI uses the sitekey and URL — both are the same.

Next Steps

Start solving CAPTCHAs in PWAs — get your CaptchaAI API key and integrate dynamic detection into your automation.

Related guides:

Discussions (0)

No comments yet.

Related Posts

Reference CAPTCHA Token Injection Methods Reference
Complete reference for injecting solved CAPTCHA tokens into web pages.

Complete reference for injecting solved CAPTCHA tokens into web pages. Covers re CAPTCHA, Turnstile, and Cloud...

Automation Python reCAPTCHA v2
Apr 08, 2026
Tutorials Pytest Fixtures for CaptchaAI API Testing
Build reusable pytest fixtures to test CAPTCHA-solving workflows with Captcha AI.

Build reusable pytest fixtures to test CAPTCHA-solving workflows with Captcha AI. Covers mocking, live integra...

Automation Python reCAPTCHA v2
Apr 08, 2026
Reference Browser Session Persistence for CAPTCHA Workflows
Manage browser sessions, cookies, and storage across CAPTCHA-solving runs to reduce repeat challenges and maintain authenticated state.

Manage browser sessions, cookies, and storage across CAPTCHA-solving runs to reduce repeat challenges and main...

Automation Python reCAPTCHA v2
Feb 24, 2026
Integrations Browser Profile Isolation + CaptchaAI Integration
Browser profile isolation tools create distinct browser environments with unique fingerprints per session.

Browser profile isolation tools create distinct browser environments with unique fingerprints per session. Com...

Automation Python reCAPTCHA v2
Feb 21, 2026
Comparisons WebDriver vs Chrome DevTools Protocol for CAPTCHA Automation
Compare Web Driver and Chrome Dev Tools Protocol (CDP) for CAPTCHA automation — detection, performance, capabilities, and when to use each with Captcha AI.

Compare Web Driver and Chrome Dev Tools Protocol (CDP) for CAPTCHA automation — detection, performance, capabi...

Automation Python reCAPTCHA v2
Mar 27, 2026
Use Cases Event Ticket Monitoring with CAPTCHA Handling
Build an event ticket availability monitor that handles CAPTCHAs using Captcha AI.

Build an event ticket availability monitor that handles CAPTCHAs using Captcha AI. Python workflow for checkin...

Automation Python reCAPTCHA v2
Jan 17, 2026
Use Cases CAPTCHA Solving in Ticket Purchase Automation
How to handle CAPTCHAs on ticketing platforms Ticketmaster, AXS, and event sites using Captcha AI for automated purchasing workflows.

How to handle CAPTCHAs on ticketing platforms Ticketmaster, AXS, and event sites using Captcha AI for automate...

Automation Python reCAPTCHA v2
Feb 25, 2026
Tutorials Caching CAPTCHA Tokens for Reuse
Cache and reuse CAPTCHA tokens with Captcha AI to reduce API calls and costs.

Cache and reuse CAPTCHA tokens with Captcha AI to reduce API calls and costs. Covers token lifetimes, cache st...

Automation Python reCAPTCHA v2
Feb 15, 2026
Tutorials CAPTCHA Retry Queue with Exponential Backoff
Implement a retry queue with exponential backoff for Captcha AI API calls.

Implement a retry queue with exponential backoff for Captcha AI API calls. Handles transient failures, rate li...

Automation Python reCAPTCHA v2
Feb 15, 2026
Tutorials Using Fiddler to Inspect CaptchaAI API Traffic
How to use Fiddler Everywhere and Fiddler Classic to capture, inspect, and debug Captcha AI API requests and responses — filters, breakpoints, and replay for tr...

How to use Fiddler Everywhere and Fiddler Classic to capture, inspect, and debug Captcha AI API requests and r...

Automation Python All CAPTCHA Types
Mar 05, 2026
Tutorials GeeTest Token Injection in Browser Automation Frameworks
how to inject Gee Test v 3 solution tokens into Playwright, Puppeteer, and Selenium — including the three-value response, callback triggering, and form submissi...

Learn how to inject Gee Test v 3 solution tokens into Playwright, Puppeteer, and Selenium — including the thre...

Automation Python Testing
Jan 18, 2026