¿La variante invisible de reCAPTCHA v2 es más difícil de resolver que la casilla de siempre? En la práctica, no: para CaptchaAI la única diferencia es añadir invisible=1 a tu solicitud. Lo que de verdad cambia aparece después, al inyectar el token en la página. Ambas comparten la misma tecnología de reCAPTCHA v2 y el mismo campo g-recaptcha-response; la diferencia es que la casilla muestra el widget "No soy un robot" y la invisible se dispara sola al hacer clic, enviar un formulario o cargar la página, sin nada visible en pantalla.
Esto importa: muchos portales — desde el checkout de una tienda hasta un portal de cita previa — muestran la casilla en escritorio y la invisible en móvil. Tu código tiene que reconocer ambas, así que conviene entender en qué se parecen y en qué no.
reCAPTCHA v2 vs reCAPTCHA invisible: comparación rápida
| Característica | v2 con casilla | v2 invisible |
|---|---|---|
| Widget visible | Sí — casilla "No soy un robot" | No — oculto al usuario |
| Cómo se dispara | El usuario marca la casilla | Clic en un botón, envío de formulario o carga de la página |
| Desafío de imagen | Aparece cuando Google sospecha del tráfico | Ventana emergente abajo a la derecha cuando hay sospecha |
| Experiencia de usuario | Fricción moderada | Fricción baja o nula |
| Implementación | Añadir el div g-recaptcha a la página |
Añadir data-callback al botón o usar grecaptcha.execute() |
| Formato del sitekey | Idéntico al de la invisible | Idéntico al de la casilla |
| Campo del token | g-recaptcha-response |
g-recaptcha-response |
| Función callback | Opcional | Casi siempre obligatoria |
| Método en CaptchaAI | method=userrecaptcha |
method=userrecaptcha + invisible=1 |
| Vida útil del token | 120 segundos | 120 segundos |
| Tiempo de resolución | 10–30 segundos | 10–25 segundos |
- Casi todo coincide: mismo sitekey, mismo campo de token, mismo método y misma vida útil.
- Solo cambian tres cosas: cómo detectas la variante, si hay que llamar al callback y unos segundos de velocidad.
Cómo aparece cada variante en el HTML
La casilla "No soy un robot"
<!-- Standard checkbox widget -->
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-callback="onSubmit">
</div>
<!-- Widget renders as: -->
<!-- [✓] I'm not a robot reCAPTCHA logo -->
La variante invisible
<!-- Pattern 1: Invisible widget on a button -->
<button class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-callback="onSubmit"
data-size="invisible">
Submit
</button>
<!-- Pattern 2: Invisible div (programmatic trigger) -->
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-size="invisible"
data-callback="onSubmit">
</div>
<!-- Pattern 3: Programmatic render -->
<script>
grecaptcha.render('submit-btn', {
sitekey: '6Le-wvkSAAAAAPBMRTvw...',
callback: onSubmit,
size: 'invisible'
});
</script>
En la casilla, el usuario ve el widget; si Google sospecha, aparece un desafío de imágenes bajo ella. En la invisible no hay widget: reCAPTCHA se activa al disparar el elemento indicado o al llamar a grecaptcha.execute(). La marca distintiva es siempre data-size="invisible".
Cómo saber qué variante usa una página
- El sitekey no te sirve: es idéntico en las dos variantes.
- Con Python: descarga el HTML y busca
data-size="invisible"con BeautifulSoup. - Con Node.js: el mismo chequeo con Axios y Cheerio.
- En la consola del navegador: una línea para confirmarlo durante el QA.
import requests
from bs4 import BeautifulSoup
import re
def detect_recaptcha_variant(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Check for invisible indicators
invisible_widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
if invisible_widget:
return {
"variant": "invisible",
"sitekey": invisible_widget.get("data-sitekey"),
"callback": invisible_widget.get("data-callback")
}
# Check for programmatic invisible in scripts
for script in soup.find_all("script"):
if script.string and "invisible" in str(script.string):
key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
if key_match:
return {
"variant": "invisible-programmatic",
"sitekey": key_match.group(1),
"callback": "check grecaptcha.render() call"
}
# Check for standard checkbox
checkbox_widget = soup.find(class_="g-recaptcha")
if checkbox_widget:
return {
"variant": "checkbox",
"sitekey": checkbox_widget.get("data-sitekey"),
"callback": checkbox_widget.get("data-callback")
}
return None
result = detect_recaptcha_variant("https://staging.example.com/qa-login")
print(result)
const axios = require("axios");
const cheerio = require("cheerio");
async function detectRecaptchaVariant(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
// Check for invisible
const invisible = $(".g-recaptcha[data-size='invisible']");
if (invisible.length) {
return {
variant: "invisible",
sitekey: invisible.attr("data-sitekey"),
callback: invisible.attr("data-callback"),
};
}
// Check scripts for programmatic invisible
const scripts = $("script")
.map((_, el) => $(el).html())
.get()
.join("\n");
if (scripts.includes("invisible")) {
const keyMatch = scripts.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
if (keyMatch) {
return {
variant: "invisible-programmatic",
sitekey: keyMatch[1],
callback: "check render call",
};
}
}
// Check for standard checkbox
const checkbox = $(".g-recaptcha");
if (checkbox.length) {
return {
variant: "checkbox",
sitekey: checkbox.attr("data-sitekey"),
callback: checkbox.attr("data-callback"),
};
}
return null;
}
const el = document.querySelector('[data-size="invisible"]');
console.log(el ? "Invisible reCAPTCHA" : "Checkbox reCAPTCHA");
| Señal de detección | v2 con casilla | v2 invisible |
|---|---|---|
data-size="invisible" |
Ausente | Presente |
| Casilla visible en la página | Sí | No |
Se llama a grecaptcha.execute() |
No (lo dispara el usuario) | Sí (de forma programática) |
| Posición del desafío emergente | En línea, bajo la casilla | Esquina inferior derecha de la página |
Resolver reCAPTCHA v2 vs invisible con CaptchaAI
Con la variante identificada, la resolución es casi igual: envías la tarea a in.php, sondeas en res.php y recuperas el token; el único parámetro que separa las dos rutas es invisible=1. El costo tampoco cambia: CaptchaAI factura por thread, no por resolución ni por tipo de CAPTCHA. Los planes arrancan en BASIC ($15/mes, 5 threads) con resoluciones ilimitadas por thread y un costo mensual predecible en USD.
v2 con casilla
import requests
import time
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAA...",
"pageurl": "https://example.com/form"
})
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
v2 invisible
import requests
import time
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAA...",
"pageurl": "https://example.com/form",
"invisible": 1 # Only parameter difference
})
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
Inyección del token: aquí está la diferencia de verdad
El token es idéntico en las dos variantes; lo que cambia es qué hace la página con él. En la casilla, el clic del usuario ya registró el callback, así que suele bastar con inyectar el token en el campo oculto. En la invisible ese clic nunca ocurrió y el formulario no se entera del token.
Casilla v2: el campo oculto suele bastar
# Selenium — inject into hidden field
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# If the page uses a callback, also call it
callback = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
if callback:
driver.execute_script(f'{callback}("{token}");')
Invisible v2: casi siempre hay que llamar al callback
# Selenium — inject AND call the callback
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# CRITICAL: Invisible reCAPTCHA almost always requires calling the callback
callback_name = driver.find_element(
"css selector", ".g-recaptcha[data-size='invisible']"
).get_attribute("data-callback")
driver.execute_script(f'{callback_name}("{token}");')
// Puppeteer — invisible callback injection
await page.evaluate((tok) => {
// Set the hidden field
document.getElementById("g-recaptcha-response").value = tok;
// Find and call the callback function
const widget = document.querySelector("[data-size='invisible']");
const cbName = widget?.getAttribute("data-callback");
if (cbName && typeof window[cbName] === "function") {
window[cbName](tok);
}
}, token);
La clave: la casilla suele funcionar solo con el campo oculto, porque el clic del usuario ya registró el callback. La invisible nunca tiene ese clic, así que el callback hay que llamarlo de forma explícita.
Errores frecuentes y cómo corregirlos
| Problema | Solución en la casilla | Solución en la invisible |
|---|---|---|
| El token no se acepta | Inyéctalo en el campo g-recaptcha-response |
Inyéctalo Y llama a la función data-callback |
| No aparece el widget | Busca la clase .g-recaptcha |
Revisa data-size="invisible" o las llamadas de render en los scripts |
| El formulario se envía pero falla | Comprueba si se espera un callback | Casi siempre hace falta el callback: localízalo y ejecútalo |
| La página se recarga tras la inyección | Falló la validación de JavaScript | Llama al callback antes de que se dispare el envío automático del formulario |
Un resolutor único para las dos variantes
Como detección y resolución comparten casi todo, conviene envolver ambas en una clase que detecte la variante, ajuste invisible=1 cuando toque y devuelva el token con su callback:
import requests
import time
from bs4 import BeautifulSoup
class RecaptchaV2UniversalSolver:
def __init__(self, api_key):
self.api_key = api_key
def detect_and_solve(self, page_url, page_html=None):
if not page_html:
page_html = requests.get(page_url).text
soup = BeautifulSoup(page_html, "html.parser")
# Detect variant
invisible = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
widget = invisible or soup.find(class_="g-recaptcha")
if not widget:
raise Exception("No reCAPTCHA widget found")
sitekey = widget.get("data-sitekey")
is_invisible = invisible is not None
callback = widget.get("data-callback")
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url
}
if is_invisible:
params["invisible"] = 1
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
return {
"token": result.text.split("|")[1],
"variant": "invisible" if is_invisible else "checkbox",
"callback": callback,
"sitekey": sitekey
}
if result.text != "CAPCHA_NOT_READY":
raise Exception(f"Solve error: {result.text}")
raise Exception("Timed out")
# Usage
solver = RecaptchaV2UniversalSolver("YOUR_API_KEY")
result = solver.detect_and_solve("https://staging.example.com/qa-login")
print(f"Variant: {result['variant']}, Callback: {result['callback']}")
Preguntas frecuentes
¿Cómo distingo la variante invisible sin leer todo el HTML?
Busca data-size="invisible" en el widget o una llamada a grecaptcha.execute() en los scripts. Si ves la casilla "No soy un robot", es la variante con casilla; si el formulario está protegido pero no ves nada, es la invisible. El sitekey no distingue: es idéntico en ambas.
¿Por qué mi token es válido pero el formulario sigue sin enviarse?
Casi siempre es el callback. En la invisible no existe el clic del usuario que registra la respuesta, así que aunque inyectes el token en g-recaptcha-response, la página no se entera hasta que ejecutas la función data-callback. Localízala en el atributo del widget y llámala.
¿reCAPTCHA v2 invisible es lo mismo que reCAPTCHA v3?
No. reCAPTCHA v2 invisible sigue siendo v2: devuelve un token que validas al enviar el formulario y aún puede mostrar un desafío de imagen. reCAPTCHA v3 no muestra desafíos y entrega una puntuación de riesgo continua. Tienes la comparación completa más abajo.
Guías relacionadas
- Cómo resolver reCAPTCHA v2 con la API — tutorial completo de la casilla v2
- Cómo funciona reCAPTCHA invisible — el mecanismo invisible por dentro
- Cómo resolver reCAPTCHA invisible con la API — tutorial de la variante invisible
- Errores comunes de reCAPTCHA invisible y sus soluciones — resolución de problemas de la invisible
- Guía de resolución de reCAPTCHA invisible con la API
- Cómo funciona reCAPTCHA invisible en detalle
- reCAPTCHA v2 vs v3 explicado