Comparativas

reCAPTCHA v2 vs reCAPTCHA invisible explicado

¿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 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

Los comentarios están deshabilitados para este artículo.