Explicaciones Técnicas

Callback de reCAPTCHA v2: cómo funciona y cómo activarlo

Inyectaste un token válido de CaptchaAI en el textarea g-recaptcha-response, pero el botón de enviar sigue en gris y el formulario no reacciona. En la gran mayoría de los casos el token no es el problema: falta disparar el callback que reCAPTCHA v2 ejecuta cuando un humano resuelve el desafío. Ese callback es el que habilita el botón, valida el formulario o lanza la petición AJAX. Si no lo llamas tú después de inyectar el token, la página nunca se entera de que el CAPTCHA ya está resuelto. En esta guía verás cómo localizar esa función y cómo activarla desde Selenium o Puppeteer.


Cómo funciona el callback de reCAPTCHA v2

El sitio declara el callback directamente en el widget de reCAPTCHA:

<div class="g-recaptcha"
     data-sitekey="6Le-SITEKEY"
     data-callback="onCaptchaSuccess"
     data-expired-callback="onCaptchaExpired">
</div>

<script>
function onCaptchaSuccess(token) {
  document.getElementById('submit-btn').disabled = false;
  document.getElementById('captcha-token').value = token;
}
</script>

Cuando el usuario resuelve el desafío, el JavaScript de Google llama a onCaptchaSuccess(token) y le pasa la cadena del token como único argumento. Dentro de esa función suele ocurrir una de estas cosas:

  • Se habilita el botón de enviar.
  • Se rellena un campo oculto con el token.
  • Se lanza una petición AJAX de validación.

Por eso, reproducir la resolución en un flujo automatizado no termina al inyectar el token: hay que replicar también esa llamada.

Consejo: guarda el nombre del callback en una variable en tiempo de ejecución; en algunos sitios cambia entre cargas de página.


Cómo localizar la función callback

Antes de disparar nada necesitas saber el nombre de la función. Un caso habitual: automatizas el QA del formulario de acceso de tu propio portal en staging —un flujo de cita previa, el checkout de una tienda tipo MercadoLibre o un panel interno— y cada ejecución se queda clavada justo en este punto.

Hay tres vías para dar con el nombre, según cómo esté expuesto:

  • Atributo data-callback: el caso más limpio, el nombre está a la vista en el HTML.
  • Opción dentro de grecaptcha.render(): el sitio lo declara por JavaScript, no en el atributo.
  • Registro interceptado: para cuando el nombre está minificado o encerrado en un closure.

Método 1: leer el atributo data-callback

// In browser console
const widget = document.querySelector('.g-recaptcha');
const callbackName = widget?.getAttribute('data-callback');
console.log('Callback:', callbackName);

Es el caso más limpio: el nombre del callback está a la vista en el atributo data-callback del div .g-recaptcha.

Método 2: inspeccionar grecaptcha.render()

Algunos sitios no usan el atributo data-callback, sino que pasan el callback como una opción dentro de grecaptcha.render():

// Search page source for grecaptcha.render
document.querySelectorAll('script:not([src])').forEach(s => {
  if (s.textContent.includes('grecaptcha.render')) {
    console.log(s.textContent.match(/callback\s*:\s*(\w+)/)?.[1]);
  }
});

Aquí recorres los scripts en línea de la página buscando la llamada a grecaptcha.render y extraes el nombre con una expresión regular.

Nota: si la página carga reCAPTCHA de forma diferida, espera a que exista grecaptcha antes de lanzar la búsqueda.

Método 3: interceptar el registro del callback

Cuando el nombre no aparece ni en el atributo ni en un script legible —por ejemplo, si está minificado o encerrado en un closure— puedes envolver grecaptcha.render para capturarlo en tiempo de ejecución. Ejecútalo en la consola de DevTools antes de que cargue la página (pestaña Sources → Snippets):

const origRender = grecaptcha.render;
grecaptcha.render = function(container, params) {
  console.log('Render callback:', params.callback);
  console.log('Expired callback:', params['expired-callback']);
  return origRender.apply(this, arguments);
};

Disparar el callback tras inyectar el token

Con el nombre localizado, el procedimiento tiene tres pasos, sea cual sea el lenguaje:

  1. Resuelve el CAPTCHA con CaptchaAI y espera a recibir el token.
  2. Escribe el token en el textarea g-recaptcha-response.
  3. Llama al callback pasándole ese token como argumento.

Python (Selenium)

import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")

# Extract sitekey and callback
sitekey = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")

callback = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-callback")

# Solve con CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": driver.current_url,
    "json": "1",
}).json()
task_id = resp["request"]

token = None
for _ in range(24):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1"
    }).json()
    if result["status"] == 1:
        token = result["request"]
        break

# Inject token into textarea
driver.execute_script("""
    document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
""", token)

# Trigger the callback
if callback:
    driver.execute_script(f"window['{callback}'](arguments[0]);", token)
    print(f"Triggered callback: {callback}")
else:
    # Fallback: try ___grecaptcha_cfg
    driver.execute_script("""
        try {
            var widgetId = Object.keys(___grecaptcha_cfg.clients)[0];
            var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
            if (typeof callback === 'function') callback(arguments[0]);
        } catch(e) {}
    """, token)
    print("Triggered callback via ___grecaptcha_cfg")

El bloque resuelve el desafío contra el endpoint in.php, sondea res.php cada cinco segundos hasta obtener el token e inyecta el resultado. A partir de ahí hay dos caminos:

  • Si el atributo data-callback existe, se llama a la función de forma directa.
  • Si no, recurre al respaldo ___grecaptcha_cfg, que recorre la configuración interna de grecaptcha hasta dar con ella.

JavaScript (Puppeteer)

El mismo flujo desde Puppeteer, práctico si tu automatización ya vive en Node.js:

Recuerda: no reutilices un token entre páginas distintas; reCAPTCHA v2 lo emite para una carga concreta.

const puppeteer = require('puppeteer');

// After solving and getting the token...
await page.evaluate((token, callbackName) => {
  // Set textarea value
  const textarea = document.querySelector(
    'textarea[name="g-recaptcha-response"]'
  );
  textarea.value = token;
  textarea.style.display = 'block'; // sometimes hidden

  // Trigger callback
  if (callbackName && typeof window[callbackName] === 'function') {
    window[callbackName](token);
    console.log(`Called ${callbackName}()`);
  } else {
    // Fallback: search grecaptcha config
    try {
      const clients = ___grecaptcha_cfg.clients;
      const widgetId = Object.keys(clients)[0];
      const cb = clients[widgetId]?.aa?.l?.callback;
      if (typeof cb === 'function') cb(token);
    } catch (e) {}
  }
}, token, callbackName);

Páginas sin data-callback

Algunas páginas no definen ningún callback. En su lugar:

  • Comprueban grecaptcha.getResponse() en el momento del envío.
  • Delegan toda la validación en el servidor tras recibir el formulario.

Para esos casos, sobrescribe la función para que devuelva tu token y envía el formulario con normalidad:

driver.execute_script("""
    const token = arguments[0];
    document.querySelector('textarea[name="g-recaptcha-response"]').value = token;
    // Override getResponse to return the token
    if (typeof grecaptcha !== 'undefined') {
        grecaptcha.getResponse = function() { return token; };
    }
""", token)

# Then submit the form normally
driver.find_element(By.CSS_SELECTOR, "form").submit()

Diagnóstico rápido

Cuando algo falla, casi siempre es una de estas cuatro causas:

Síntoma Causa probable Solución
El formulario sigue deshabilitado tras inyectar el token No se disparó el callback Localiza y llama a la función callback
ReferenceError: function not defined El callback está definido dentro de un closure Usa el respaldo ___grecaptcha_cfg
El token se inyecta pero no sale la petición AJAX El callback lanza el AJAX, no el envío del formulario Revisa qué hace exactamente la función callback
El token se acepta pero la página muestra un error El token caducó antes de disparar el callback Resuelve más cerca del momento de envío

Callbacks de expiración

Otros sitios definen data-expired-callback para volver a deshabilitar el botón cuando el token caduca. Ten presente que:

  • Si resuelves demasiado pronto, ese callback puede dispararse y bloquear de nuevo el formulario.
  • La regla práctica es resolver lo más cerca posible del momento de envío.

Así compruebas si el sitio lo define:

// Check for expired callback
const expiredCallback = document.querySelector('.g-recaptcha')
  ?.getAttribute('data-expired-callback');
console.log('Expired callback:', expiredCallback);

Preguntas frecuentes

¿Por qué el botón de enviar sigue gris si el token es válido?

Porque el token por sí solo no notifica al formulario. reCAPTCHA v2 habilita el botón dentro del callback, así que hasta que no ejecutes esa función la página asume que nadie ha resuelto el desafío. Inyecta el token y, acto seguido, dispara el callback.

¿Cómo distingo un sitio que usa callback de uno que solo valida en el backend?

  • Si el div .g-recaptcha tiene atributo data-callback o hay una llamada a grecaptcha.render en los scripts, el sitio usa callback: dispáralo tras inyectar el token.
  • Si no existe ninguno de los dos, lo más probable es que valide grecaptcha.getResponse() en el servidor al enviar; basta con inyectar el token y enviar el formulario.

¿Puedo probar todo esto con el plan más económico de CaptchaAI?

Sí. El plan BASIC ($15/mes, 5 threads) es suficiente para desarrollar y depurar la integración, y puedes escalar a STANDARD ($30/mes, 15 threads) o superior cuando crezca tu volumen. La facturación es por thread, con resoluciones ilimitadas dentro de cada plan.

¿reCAPTCHA v3 utiliza este mismo callback?

No. reCAPTCHA v3 se ejecuta con grecaptcha.execute(), que devuelve una Promise y no muestra ningún widget visible ni atributo data-callback. Su integración y su manejo del token son distintos.


Guías relacionadas


Resuelve reCAPTCHA v2 con el callback bien gestionado usando CaptchaAI

Crea tu cuenta y obtén tu API key en captchaai.com. Inyecta el token, dispara el callback y deja que tu formulario avance sin fricciones.

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