Troubleshooting

Common reCAPTCHA Invisible Errors and Fixes

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

  1. grecaptcha.execute() fires the invisible challenge
  2. After solving, Google calls the function specified in data-callback
  3. 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 siteverify returns timeout-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

Discussions (0)

No comments yet.