Tutorials

reCAPTCHA in Single Page Applications: Dynamic Loading Patterns

Single page applications load reCAPTCHA dynamically — the widget doesn't exist in the initial HTML. A React login form renders the CAPTCHA only when the component mounts. A Vue checkout page loads reCAPTCHA after the user clicks "Place Order." Traditional page-source scraping misses these entirely. Here's how to detect and solve dynamically loaded reCAPTCHAs.

Why SPAs Are Different

Traditional page SPA
reCAPTCHA script in initial HTML Script injected after route change
Widget renders on page load Widget renders on component mount
Site key in page source Site key in JavaScript bundle
Form submits via POST Form submits via XHR/fetch

In a SPA, the reCAPTCHA widget may not exist until a specific user action triggers it. Your automation must wait for the widget to appear.

Detecting the reCAPTCHA Widget

Wait for the Element

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://example.com/login")

    # SPA may need a click or navigation to trigger reCAPTCHA
    page.click("#show-login-form")

    # Wait for reCAPTCHA iframe to appear
    recaptcha_frame = page.wait_for_selector(
        "iframe[src*='recaptcha']",
        timeout=15000
    )
    print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))

Extract the Site Key

The site key can be in multiple locations in a SPA:

# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None

# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
    "[data-sitekey]",
    "el => el.getAttribute('data-sitekey')"
)

# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
    () => {
        // Check for grecaptcha render calls
        const scripts = document.querySelectorAll('script');
        for (const s of scripts) {
            const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
            if (match) return match[2];
        }
        return null;
    }
""")

Solving the reCAPTCHA

Once you have the site key, submit to CaptchaAI:

import requests
import time

def solve_recaptcha(site_key, page_url):
    # Submit task
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    # Poll for result
    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

Injecting the Token in a SPA

SPAs don't submit traditional forms. You need to inject the token where the application expects it.

Method 1: Set the Hidden Textarea

token = solve_recaptcha(site_key, "https://example.com/login")

# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

# Submit the form
page.click("#login-button")

Method 2: Trigger the Callback

Many SPA reCAPTCHA implementations use a callback function:

# Find the callback function name
callback = page.evaluate("""
    () => {
        const widget = document.querySelector('.g-recaptcha');
        return widget?.getAttribute('data-callback') || null;
    }
""")

# Call it with the token
if callback:
    page.evaluate(f"window['{callback}']('{token}')")
else:
    # Try the default grecaptcha callback
    page.evaluate(f"""
        if (window.grecaptcha) {{
            // Set the response and trigger verification
            document.getElementById('g-recaptcha-response').value = '{token}';
            // Find and call any registered callbacks
            const form = document.querySelector('form');
            if (form) form.dispatchEvent(new Event('submit'));
        }}
    """)

Method 3: Intercept the XHR (JavaScript / Puppeteer)

const puppeteer = require('puppeteer');

async function solveRecaptchaInSPA() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Intercept the API call and inject the token
  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url().includes('/api/login')) {
      const postData = JSON.parse(request.postData() || '{}');
      // Token already set via page.evaluate — let it pass
      request.continue();
    } else {
      request.continue();
    }
  });

  await page.goto('https://example.com/login');
  await page.waitForSelector('[data-sitekey]');

  const siteKey = await page.$eval(
    '[data-sitekey]',
    el => el.getAttribute('data-sitekey')
  );

  // Solve with CaptchaAI (implementation omitted — same as Python)
  const token = await solveCaptcha(siteKey, page.url());

  // Inject token and trigger callback
  await page.evaluate((t) => {
    document.getElementById('g-recaptcha-response').value = t;
    const callback = document.querySelector('.g-recaptcha')
      ?.getAttribute('data-callback');
    if (callback && window[callback]) {
      window[callback](t);
    }
  }, token);
}

Framework-Specific Patterns

React (react-google-recaptcha)

React apps often use the react-google-recaptcha package. The component renders asynchronously:

# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")

# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
    ".g-recaptcha", "el => el.dataset.sitekey"
)

Vue (vue-recaptcha)

# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")

Angular

# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")

Handling Route Changes

SPAs reuse the page — navigation happens without full reloads:

# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")

# Navigate within the SPA
page.click("a[href='/login']")

# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)

Troubleshooting

Issue Cause Fix
g-recaptcha-response textarea not found SPA hasn't rendered the widget yet Use wait_for_selector with adequate timeout
Token injected but form doesn't submit Callback not triggered Find and call the data-callback function
Site key not in page source Bundled in JS or loaded dynamically Wait for widget render, then extract from iframe src
Token works once then fails SPA re-renders component, clearing token Inject token immediately before form submission
Widget re-renders after token injection React/Vue reactivity clears the value Use data-callback approach instead of setting textarea

FAQ

Can I skip the browser and solve SPA reCAPTCHAs with just HTTP requests?

If you can extract the site key and replicate the form submission API call, yes. Many SPA forms submit via a REST API endpoint — you can call it directly with the token. But you need a browser to discover the site key and API endpoint first.

How do I handle reCAPTCHA that only appears on certain user actions?

Automate the triggering action (click a button, fill a form, navigate to a route), then wait for the reCAPTCHA element to appear. Use wait_for_selector with a reasonable timeout.

Does CaptchaAI care whether the reCAPTCHA is in a SPA?

No. CaptchaAI only needs the site key and page URL. How the widget loads on the page doesn't affect solving.

Next Steps

Solve dynamically loaded reCAPTCHAs in any SPA — get your CaptchaAI API key.

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.