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.
Related Articles
- How To Solve Recaptcha V2 Callback Using Api
- Recaptcha V2 Turnstile Same Site Handling
- Recaptcha V2 Callback Mechanism
Next Steps
Solve dynamically loaded reCAPTCHAs in any SPA — get your CaptchaAI API key.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.