Invisible reCAPTCHA removes the checkbox — but adds new failure modes for automation. The biggest problems are not detecting the invisible widget at all, missing the callback function, submitting expired tokens, and using v2 standard parameters when invisible parameters are required.
This guide covers every common error pattern with the exact fix. If you need background on how invisible reCAPTCHA works, read How reCAPTCHA Invisible Works and How to Solve It first.
Quick error reference
| Error | Cause | Fix |
|---|---|---|
ERROR_WRONG_GOOGLEKEY |
Wrong sitekey or sitekey from a different domain | Extract sitekey from the invisible widget div or grecaptcha.render() call |
ERROR_PAGEURL |
URL mismatch — sent parent page URL instead of iframe URL | Use the exact URL where the invisible widget loads |
ERROR_CAPTCHA_UNSOLVABLE |
Google flagged the task as impossible | Retry with fresh proxy and cookies; check if site switched to v3 |
ERROR_BAD_TOKEN_OR_PAGEURL |
Token rejected by target site | Verify pageurl matches exactly; inject via callback, not hidden field |
CAPCHA_NOT_READY |
Task still processing | Keep polling every 5 seconds; invisible solves take 10-30 seconds |
ERROR_KEY_DOES_NOT_EXIST |
Invalid CaptchaAI API key | Check key at captchaai.com/account |
| Token accepted but form fails | Callback not executed after token injection | Find and call the data-callback function with the token |
Error 1: Not detecting invisible reCAPTCHA on the page
Invisible reCAPTCHA has no visible checkbox. If your scraper does not detect it, captcha-protected requests fail silently with form submission errors or redirects.
How to detect invisible reCAPTCHA
Look for these patterns in the page HTML:
<!-- Pattern 1: div with data-size="invisible" -->
<div class="g-recaptcha" data-sitekey="6LdKlZEU..."
data-size="invisible"
data-callback="onSubmit"></div>
<!-- Pattern 2: button with data-sitekey and invisible size -->
<button class="g-recaptcha"
data-sitekey="6LdKlZEU..."
data-callback="onSubmit"
data-action="submit">Submit</button>
<!-- Pattern 3: programmatic render with size: invisible -->
<script>
grecaptcha.render('submit-btn', {
sitekey: '6LdKlZEU...',
callback: onSubmit,
size: 'invisible'
});
</script>
Detection script (Python):
import requests
from bs4 import BeautifulSoup
import re
def detect_invisible_recaptcha(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Check for data-size="invisible"
widget = soup.find("div", {"data-size": "invisible", "class": "g-recaptcha"})
if widget:
return {
"type": "invisible",
"sitekey": widget.get("data-sitekey"),
"callback": widget.get("data-callback")
}
# Check for programmatic render with invisible
scripts = soup.find_all("script")
for script in scripts:
if script.string and "size" in str(script.string) and "invisible" in str(script.string):
key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
if key_match:
return {
"type": "invisible-programmatic",
"sitekey": key_match.group(1),
"callback": "check grecaptcha.render() call"
}
return None
Detection script (Node.js):
const axios = require("axios");
const cheerio = require("cheerio");
async function detectInvisibleRecaptcha(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
// Check for data-size="invisible"
const widget = $(".g-recaptcha[data-size='invisible']");
if (widget.length) {
return {
type: "invisible",
sitekey: widget.attr("data-sitekey"),
callback: widget.attr("data-callback"),
};
}
// Check script tags for programmatic invisible render
const scriptContent = $("script")
.map((_, el) => $(el).html())
.get()
.join("\n");
if (scriptContent.includes("invisible")) {
const keyMatch = scriptContent.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
if (keyMatch) {
return {
type: "invisible-programmatic",
sitekey: keyMatch[1],
callback: "check grecaptcha.render() call",
};
}
}
return null;
}
Error 2: Wrong sitekey — ERROR_WRONG_GOOGLEKEY
This happens when you send a sitekey that does not match the invisible reCAPTCHA widget on the target page. Common causes:
- Copied the sitekey from a v2 checkbox on a different page
- Used a sitekey from the anchor URL of a different reCAPTCHA version
- Page has multiple reCAPTCHA widgets and you grabbed the wrong one
Fix: Extract the correct invisible sitekey
import requests
from bs4 import BeautifulSoup
def get_invisible_sitekey(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Priority 1: invisible widget
widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
if widget:
return widget["data-sitekey"]
# Priority 2: any g-recaptcha div (may be invisible without data-size)
widget = soup.find(class_="g-recaptcha")
if widget and widget.get("data-sitekey"):
return widget["data-sitekey"]
return None
sitekey = get_invisible_sitekey("https://example.com/login")
print(f"Sitekey: {sitekey}")
Error 3: Callback not executed — form submits but nothing happens
This is the number one invisible reCAPTCHA failure that developers miss. Unlike v2 checkbox where injecting the token into g-recaptcha-response is enough, invisible reCAPTCHA almost always uses a JavaScript callback function. If you inject the token but do not call the callback, the form never processes.
How the callback flow works
grecaptcha.execute()fires the invisible challenge- After solving, Google calls the function specified in
data-callback - That callback function submits the form or makes the API call
Fix: Find and execute the callback
Step 1 — Identify the callback name:
# From HTML: data-callback="onSubmit"
# From JS: callback: onSubmit
# From grecaptcha.render: second argument with callback property
Step 2 — Inject token AND call the callback (Selenium):
from selenium import webdriver
import requests
import time
driver = webdriver.Chrome()
driver.get("https://example.com/form")
# Get sitekey
sitekey = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-sitekey")
callback_name = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
# Solve with CaptchaAI
task_id = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": driver.current_url,
"invisible": 1
}).text.split("|")[1]
# Poll for result
token = None
for _ in range(60):
time.sleep(5)
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id
}).text
if resp.startswith("OK|"):
token = resp.split("|")[1]
break
# Inject token into the response field
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# CRITICAL: Call the callback function
driver.execute_script(f'{callback_name}("{token}");')
Step 2 — Inject token AND call the callback (Puppeteer):
const puppeteer = require("puppeteer");
const axios = require("axios");
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.goto("https://example.com/form");
// Get sitekey and callback
const { sitekey, callback } = await page.evaluate(() => {
const el = document.querySelector(".g-recaptcha[data-size='invisible']");
return {
sitekey: el?.getAttribute("data-sitekey"),
callback: el?.getAttribute("data-callback"),
};
});
// Submit to CaptchaAI
const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
params: {
key: "YOUR_API_KEY",
method: "userrecaptcha",
googlekey: sitekey,
pageurl: page.url(),
invisible: 1,
},
});
const taskId = submitResp.data.split("|")[1];
// Poll for result
let token;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: "YOUR_API_KEY", action: "get", id: taskId },
});
if (result.data.startsWith("OK|")) {
token = result.data.split("|")[1];
break;
}
}
// Inject token and fire callback
await page.evaluate(
(tok, cb) => {
document.getElementById("g-recaptcha-response").value = tok;
if (cb && typeof window[cb] === "function") {
window[cb](tok);
}
},
token,
callback,
);
await browser.close();
})();
Error 4: Missing invisible=1 parameter
When solving invisible reCAPTCHA through CaptchaAI, you must include invisible=1 in your request. Without it, the solver treats the task as a standard v2 checkbox challenge, which can lead to ERROR_CAPTCHA_UNSOLVABLE or tokens that the target site rejects.
Wrong vs correct request
# WRONG — missing invisible=1
params = {
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url
}
# CORRECT — includes invisible=1
params = {
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1 # Required for invisible reCAPTCHA
}
response = requests.get("https://ocr.captchaai.com/in.php", params=params)
Error 5: Token expired before submission
Invisible reCAPTCHA tokens expire in 120 seconds — the same as v2 standard. But invisible workflows often have additional processing steps between solving and submission, making expiration more likely.
Symptoms
- Form returns a generic error after token injection
- Server-side
siteverifyreturnstimeout-or-duplicate - Token was valid but took too long to reach the submit step
Fix: Just-in-time solving
Only request the solve when you are ready to submit immediately:
import requests
import time
def solve_invisible_recaptcha(api_key, sitekey, page_url):
# Submit task
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
})
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key,
"action": "get",
"id": task_id
})
if result.text.startswith("OK|"):
return result.text.split("|")[1]
if result.text != "CAPCHA_NOT_READY":
raise Exception(f"Solve failed: {result.text}")
raise Exception("Solve timed out after 5 minutes")
# Usage: solve JUST before you need to submit
# 1. Navigate to page and prepare form data first
# 2. THEN solve the captcha
# 3. Inject token and submit immediately
token = solve_invisible_recaptcha("YOUR_API_KEY", sitekey, page_url)
# Submit within 120 seconds of receiving the token
Error 6: Token rejected — ERROR_BAD_TOKEN_OR_PAGEURL
The target site verified the token with Google and got a failure. Common causes:
| Cause | How to identify | Fix |
|---|---|---|
Wrong pageurl |
URL does not match the domain in the sitekey registration | Use the exact URL where the widget loads |
| Token used on different domain | Cross-domain token reuse | Solve with the correct domain's pageurl |
| Token already used | Submitting the same token twice | Request a new solve for each submission |
| IP mismatch | Your IP differs from the solver's IP | Add your proxy parameter to match the session IP |
| Invisible flag missing | Solved as v2 standard, used on invisible page | Add invisible=1 to the solve request |
Debug checklist
def debug_invisible_solve(api_key, sitekey, page_url, proxy=None):
"""Run a diagnostic solve with detailed logging."""
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")
print(f"Proxy: {proxy or 'none'}")
params = {
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
}
if proxy:
params["proxy"] = proxy
params["proxytype"] = "HTTP"
# Submit
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
print(f"Submit response: {resp.text}")
if not resp.text.startswith("OK|"):
return None
task_id = resp.text.split("|")[1]
print(f"Task ID: {task_id}")
# Poll with timing
start = time.time()
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key, "action": "get", "id": task_id
})
elapsed = time.time() - start
print(f" [{elapsed:.0f}s] {result.text[:50]}")
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
print(f"Token received after {elapsed:.0f}s")
print(f"Token length: {len(token)} characters")
print(f"Token starts with: {token[:30]}...")
return token
if result.text != "CAPCHA_NOT_READY":
print(f"FAILED: {result.text}")
return None
print("TIMEOUT after 5 minutes")
return None
Error 7: Multiple reCAPTCHA widgets on one page
Some pages have both a visible v2 checkbox AND an invisible reCAPTCHA. If you solve the wrong one, the token is valid but does not match the widget guarding the action you need.
Fix: Target the correct widget
from bs4 import BeautifulSoup
def find_all_recaptcha_widgets(html):
soup = BeautifulSoup(html, "html.parser")
widgets = []
for el in soup.find_all(class_="g-recaptcha"):
widgets.append({
"sitekey": el.get("data-sitekey"),
"size": el.get("data-size", "normal"),
"callback": el.get("data-callback"),
"tag": el.name,
"id": el.get("id")
})
return widgets
# Example output:
# [
# {"sitekey": "6LdA...", "size": "normal", "callback": None, "tag": "div", "id": "recaptcha-login"},
# {"sitekey": "6LdB...", "size": "invisible", "callback": "onRegister", "tag": "div", "id": "recaptcha-register"}
# ]
# Use the widget with size="invisible" for the invisible solve
Complete invisible reCAPTCHA solver with error handling
This production-ready wrapper handles all the errors above:
import requests
import time
import logging
logger = logging.getLogger(__name__)
class InvisibleRecaptchaSolver:
def __init__(self, api_key, max_retries=3):
self.api_key = api_key
self.max_retries = max_retries
self.base_url = "https://ocr.captchaai.com"
def solve(self, sitekey, page_url, proxy=None):
"""Solve invisible reCAPTCHA with automatic retry on transient errors."""
for attempt in range(1, self.max_retries + 1):
try:
token = self._attempt_solve(sitekey, page_url, proxy)
if token:
return token
except Exception as e:
logger.warning(f"Attempt {attempt} failed: {e}")
if attempt < self.max_retries:
time.sleep(2 ** attempt)
raise Exception(f"Failed to solve after {self.max_retries} attempts")
def _attempt_solve(self, sitekey, page_url, proxy):
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
}
if proxy:
params["proxy"] = proxy
params["proxytype"] = "HTTP"
# Submit task
resp = requests.get(f"{self.base_url}/in.php", params=params)
if "ERROR" in resp.text:
error = resp.text.strip()
if error in ("ERROR_WRONG_GOOGLEKEY", "ERROR_KEY_DOES_NOT_EXIST"):
raise Exception(f"Configuration error (do not retry): {error}")
if error == "ERROR_ZERO_BALANCE":
raise Exception("Account balance is zero — add funds")
raise Exception(f"Submit error: {error}")
if not resp.text.startswith("OK|"):
raise Exception(f"Unexpected submit response: {resp.text}")
task_id = resp.text.split("|")[1]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get(f"{self.base_url}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id
})
if result.text.startswith("OK|"):
return result.text.split("|")[1]
if result.text == "CAPCHA_NOT_READY":
continue
if result.text == "ERROR_CAPTCHA_UNSOLVABLE":
logger.warning("Captcha unsolvable — will retry with new task")
return None
raise Exception(f"Poll error: {result.text}")
raise Exception("Solve timed out after 5 minutes")
# Usage
solver = InvisibleRecaptchaSolver("YOUR_API_KEY")
token = solver.solve(
sitekey="6LdKlZEU...",
page_url="https://example.com/login"
)
print(f"Token: {token[:50]}...")
const axios = require("axios");
class InvisibleRecaptchaSolver {
constructor(apiKey, maxRetries = 3) {
this.apiKey = apiKey;
this.maxRetries = maxRetries;
this.baseUrl = "https://ocr.captchaai.com";
}
async solve(sitekey, pageUrl, proxy) {
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const token = await this._attemptSolve(sitekey, pageUrl, proxy);
if (token) return token;
} catch (err) {
console.warn(`Attempt ${attempt} failed: ${err.message}`);
if (attempt < this.maxRetries) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
}
throw new Error(`Failed to solve after ${this.maxRetries} attempts`);
}
async _attemptSolve(sitekey, pageUrl, proxy) {
const params = {
key: this.apiKey,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageUrl,
invisible: 1,
};
if (proxy) {
params.proxy = proxy;
params.proxytype = "HTTP";
}
// Submit task
const submitResp = await axios.get(`${this.baseUrl}/in.php`, { params });
if (submitResp.data.includes("ERROR")) {
const error = submitResp.data.trim();
if (["ERROR_WRONG_GOOGLEKEY", "ERROR_KEY_DOES_NOT_EXIST"].includes(error)) {
throw new Error(`Configuration error (do not retry): ${error}`);
}
throw new Error(`Submit error: ${error}`);
}
const taskId = submitResp.data.split("|")[1];
// Poll for result
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get(`${this.baseUrl}/res.php`, {
params: { key: this.apiKey, action: "get", id: taskId },
});
if (result.data.startsWith("OK|")) {
return result.data.split("|")[1];
}
if (result.data === "CAPCHA_NOT_READY") continue;
if (result.data === "ERROR_CAPTCHA_UNSOLVABLE") return null;
throw new Error(`Poll error: ${result.data}`);
}
throw new Error("Solve timed out after 5 minutes");
}
}
// Usage
const solver = new InvisibleRecaptchaSolver("YOUR_API_KEY");
solver.solve("6LdKlZEU...", "https://example.com/login").then((token) => {
console.log(`Token: ${token.substring(0, 50)}...`);
});
Troubleshooting checklist
Run through this checklist when invisible reCAPTCHA solving fails:
| Step | Check | Command/Action |
|---|---|---|
| 1 | Confirm it is invisible, not v2 standard | Look for data-size="invisible" or size: 'invisible' in render call |
| 2 | Verify sitekey is correct | Compare with data-sitekey on the invisible widget specifically |
| 3 | Confirm invisible=1 in API request |
Check your in.php parameters |
| 4 | Check pageurl matches exactly |
Use browser DevTools URL, not a redirect URL |
| 5 | Find the callback function name | Look for data-callback attribute or callback in grecaptcha.render() |
| 6 | Verify token injection + callback call | Both steps are required — token alone is not enough |
| 7 | Check token freshness | Token must be used within 120 seconds |
| 8 | Test with proxy if IP matters | Add proxy and proxytype parameters |
FAQ
How is invisible reCAPTCHA different from v2 standard for solving?
The API method is the same (method=userrecaptcha), but you must add invisible=1 to your request. The critical difference is on the injection side: invisible reCAPTCHA almost always requires calling a JavaScript callback function after injecting the token, while v2 standard usually works with just the hidden field.
Why does my token work in testing but fail in production?
Most likely an IP mismatch. In testing, the solver and your browser may share similar IPs. In production, the solver's IP and your server's IP differ. Add a proxy parameter that matches your session IP to fix this.
How long does invisible reCAPTCHA take to solve?
Typical solve times are 10-30 seconds through CaptchaAI. Invisible challenges are generally faster than v2 checkbox challenges because they do not require image recognition — they rely on risk analysis.
Can I solve invisible reCAPTCHA without a browser?
Yes. Since the solve happens server-side through the API, you only need the sitekey and pageurl. The browser is only needed if you must execute the callback function on the actual page. For pure API workflows, extract the sitekey, solve via CaptchaAI, and submit the token with your HTTP request.
Next steps
- How reCAPTCHA Invisible Works and How to Solve It — background on the invisible mechanism
- How to Solve reCAPTCHA v2 Using API — standard v2 solving for comparison
- Common reCAPTCHA v2 Solving Errors and Fixes — v2 standard error patterns
- CaptchaAI Error Codes: Complete Reference — full error code table
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.