¿El token de reCAPTCHA Invisible se resuelve, pero el formulario se queda quieto? Casi siempre el problema no es la resolución: es que falta disparar la función callback tras inyectar el token. En la casilla v2 basta con rellenar g-recaptcha-response; la versión invisible espera además una llamada a un callback de JavaScript.
Si quieres el mecanismo por dentro, empieza por cómo funciona reCAPTCHA Invisible. Los demás fallos habituales son estos:
- No detectar el widget invisible, porque no hay casilla que buscar.
- Olvidar
invisible=1en la solicitud a la API. - Enviar un token ya caducado, fácil cuando hay pasos intermedios.
- Usar el sitekey de un widget que no es el que protege la acción.
Caso típico: un equipo hace QA de un portal de cita previa o de un login corporativo contra su entorno de staging y en local todo va bien. En el servidor, el formulario empieza a rechazar envíos sin mensaje claro. La causa suele ser el callback que no se ejecuta o una IP que ya no coincide con la del solver.
Errores de reCAPTCHA Invisible de un vistazo
| Error | Causa | Solución |
|---|---|---|
ERROR_WRONG_GOOGLEKEY |
Sitekey incorrecto o de un dominio diferente | Extrae el sitekey del div del widget invisible o de la llamada grecaptcha.render() |
ERROR_PAGEURL |
La URL no coincide: se envió la URL de la página principal en lugar de la del iframe | Usa la URL exacta donde se carga el widget invisible |
ERROR_CAPTCHA_UNSOLVABLE |
Google marcó la tarea como imposible | Reintenta con proxy y cookies nuevos; verifica si el sitio cambió a v3 |
ERROR_BAD_TOKEN_OR_PAGEURL |
Token rechazado por el sitio de destino | Verifica que la URL de la página coincida exactamente; inyecta mediante callback, no campo oculto |
CAPCHA_NOT_READY |
La tarea aún se está procesando | Sigue sondeando cada 5 segundos; las soluciones invisibles tardan entre 10 y 30 segundos |
ERROR_KEY_DOES_NOT_EXIST |
Clave API CaptchaAI no válida | Comprueba la clave en captchaai.com/account |
| Token aceptado pero el formulario falla | Callback no ejecutado tras inyectar el token | Busca y llama a la función data-callback con el token |
Error 1: el scraper no detecta el reCAPTCHA Invisible
El reCAPTCHA invisible no muestra ninguna casilla, y cuando el scraper no lo detecta el fallo es silencioso:
- Las solicitudes protegidas se caen sin aviso.
- Ves errores de envío de formulario o redirecciones inesperadas.
Cómo identificar un reCAPTCHA Invisible en el HTML
Rastrea estas tres señales en el marcado de la página:
- Un
divcon la claseg-recaptchay el atributodata-size="invisible". - Un
buttoncondata-sitekeyque dispara el challenge al pulsarlo. - Una llamada a
grecaptcha.render()consize: 'invisible'dentro de un<script>.
<!-- 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>
Detección con 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
Detección con 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: sitekey equivocado — ERROR_WRONG_GOOGLEKEY
Aparece cuando el sitekey que envías no corresponde al widget invisible de la página. Las causas más frecuentes:
- Copiaste el sitekey de una casilla v2 que estaba en otra página.
- Tomaste un sitekey de la URL de anclaje de una versión distinta de reCAPTCHA.
- La página tiene varios widgets reCAPTCHA y agarraste el que no era.
Solución: extrae el sitekey del widget invisible correcto
Da prioridad al widget con data-size="invisible" y, si no lo encuentra, cae sobre cualquier div con clase g-recaptcha:
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://staging.example.com/qa-login")
print(f"Sitekey: {sitekey}")
Error 3: el callback no se ejecuta — el formulario se envía sin efecto
Este es el fallo número uno del reCAPTCHA invisible y el que más se pasa por alto. En v2 basta con inyectar el token en g-recaptcha-response; en la versión invisible casi siempre hay una función callback de JavaScript de por medio. Si inyectas el token pero no la llamas, el formulario no se procesa por muy válido que sea el token.
Cómo funciona el flujo del callback
grecaptcha.execute()lanza el challenge invisible- Tras resolver, Google llama a la función indicada en
data-callback - Esa función callback envía el formulario o realiza la llamada a la API
Solución: localiza el callback y dispáralo tú
Paso 1: averigua el nombre del callback
# From HTML: data-callback="onSubmit"
# From JS: callback: onSubmit
# From grecaptcha.render: second argument with callback property
Paso 2 (Selenium): inyecta el token y llama al callback
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 con 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}");')
Paso 2 (Puppeteer): inyecta el token y llama al callback
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: falta el parámetro invisible=1
Cuando resuelves reCAPTCHA invisible con CaptchaAI tienes que incluir invisible=1 en la solicitud. Sin él, el solver trata la tarea como una casilla v2 estándar, y eso deriva en dos síntomas concretos:
ERROR_CAPTCHA_UNSOLVABLE, porque el challenge invisible no encaja con el modo v2.- Tokens que se resuelven pero que el sitio de destino rechaza al verificarlos.
Solicitud incorrecta frente a correcta
# 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: el token caduca antes del envío
Los tokens de reCAPTCHA invisible caducan a los 120 segundos, igual que los de v2 estándar. El problema es que los flujos invisibles suelen tener pasos de procesamiento adicionales entre la resolución y el envío, así que la caducidad es mucho más fácil de que te pille.
Síntomas
- El formulario devuelve un error genérico después de inyectar el token.
- El
siteverifydel lado del servidor respondetimeout-or-duplicate. - El token era válido, pero tardó demasiado en llegar al paso de envío.
Solución: resolución justo a tiempo (just-in-time)
Pide la resolución solo cuando ya tengas todo listo para enviar acto seguido:
- Prepara los datos del formulario antes de resolver.
- Resuelve el CAPTCHA justo después.
- Inyecta el token y envía dentro de la ventana de 120 segundos.
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 rechazado — ERROR_BAD_TOKEN_OR_PAGEURL
El sitio verificó el token con Google y recibió un error. Causas habituales:
| Causa | Cómo identificarla | Solución |
|---|---|---|
pageurl incorrecto |
La URL no coincide con el dominio del sitekey | Usa la URL exacta donde se carga el widget |
| Token usado en un dominio diferente | Reutilización de tokens entre dominios | Resuelve con el pageurl del dominio correcto |
| Token ya utilizado | Enviar el mismo token dos veces | Solicita una nueva solución para cada envío |
| IP no coincide | Tu IP difiere de la IP del solver | Añade tu parámetro proxy para que coincida con la IP de la sesión |
| Falta el parámetro invisible | Resuelto como v2 estándar, usado en página invisible | Añade invisible=1 a la solicitud de resolución |
Solución: depura con registro detallado
Ten a mano los datos que suelen delatar la causa:
- El sitekey exacto y el
pageurlque envías. - Si usas proxy y de qué tipo (
HTTP,SOCKS5). - El tiempo total hasta recibir el token y su longitud.
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: varios widgets reCAPTCHA en la misma página
Hay páginas con una casilla v2 visible Y un reCAPTCHA invisible a la vez. Si resuelves el que no toca, el token es válido pero no corresponde al widget que protege tu acción. Para distinguirlos:
- Enumera todos los
g-recaptchay mira sudata-size. - Quédate con el que tenga
size="invisible". - Guarda el
data-callbackde cada uno: no es el mismo para login que para registro.
Solución: apunta al widget correcto
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
Solver completo de reCAPTCHA Invisible con manejo de errores
Este contenedor listo para producción reúne todo lo anterior. Se encarga de:
- Reintentar con retroceso exponencial ante errores transitorios.
- Separar los errores de configuración (no reintentar) de los pasajeros.
- Devolver
NoneanteERROR_CAPTCHA_UNSOLVABLEpara lanzar una tarea nueva.
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://staging.example.com/qa-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://staging.example.com/qa-login").then((token) => {
console.log(`Token: ${token.substring(0, 50)}...`);
});
Lista de comprobación de diagnóstico
Cuando falle la resolución del reCAPTCHA invisible, repasa estos ocho puntos en orden:
| Paso | Verifica | Comando/Acción |
|---|---|---|
| 1 | Que sea invisible, no v2 estándar | Busca data-size="invisible" o size: 'invisible' en la llamada de renderizado |
| 2 | Que el sitekey sea el correcto | Compáralo con el data-sitekey del widget invisible en concreto |
| 3 | Que invisible=1 esté en la solicitud a la API |
Revisa tus parámetros en in.php |
| 4 | Que pageurl coincida exactamente |
Usa la URL de DevTools del navegador, no una URL de redirección |
| 5 | El nombre de la función callback | Busca el atributo data-callback o callback en grecaptcha.render() |
| 6 | Inyección del token + llamada al callback | Los dos pasos son obligatorios: el token por sí solo no basta |
| 7 | La frescura del token | El token debe usarse dentro de los 120 segundos |
| 8 | Con proxy si la IP importa | Añade los parámetros proxy y proxytype |
Cómo confirmar que la resolución fue válida
Antes de dar por bueno un envío, comprueba estas cuatro señales:
- El
in.phpdevolvióOK|con untask_id, no unERROR_. - El
res.phpacabó devolviendoOK|con un token largo. - El campo
g-recaptcha-responsequedó relleno en el DOM. - El callback se ejecutó: el formulario avanzó o salió la petición.
Preguntas frecuentes
¿Cómo sé si una página usa reCAPTCHA Invisible o la casilla v2?
Busca en el HTML un g-recaptcha con data-size="invisible" o una llamada a grecaptcha.render() con size: 'invisible'. Si no hay casilla y el challenge se dispara al enviar, es invisible. La v2 clásica muestra el recuadro "No soy un robot" en la página.
¿Necesito un proxy para resolver reCAPTCHA Invisible?
Depende. En local suele funcionar sin proxy porque tu IP y la del solver quedan cerca. En producción difieren, y ahí aparece ERROR_BAD_TOKEN_OR_PAGEURL. Cuando el sitio ata el token a la IP, añade proxy y proxytype para que la resolución use tu misma salida de red.
¿Qué falta si el token se acepta pero el formulario no se envía?
Casi seguro que el callback. En la versión invisible no basta con rellenar g-recaptcha-response: tienes que llamar a la función de data-callback pasándole el token. Localiza ese nombre en el widget y ejecútalo con driver.execute_script() (Selenium) o page.evaluate() (Puppeteer) tras inyectar el token.
¿Cuánto cuesta resolver reCAPTCHA Invisible con CaptchaAI?
Los planes son por threads (hilos concurrentes), no por resolución, así que el invisible no lleva recargo por tipo. BASIC cuesta $15/mes con 5 threads y resoluciones ilimitadas por thread; si necesitas más concurrencia, ADVANCE ofrece 50 threads por $90/mes.
Artículos relacionados
- Cómo funciona reCAPTCHA Invisible por dentro
- Cómo resolver reCAPTCHA v2 con la API
- Errores comunes al resolver reCAPTCHA v2
- Referencia de códigos de error de CaptchaAI