Para resolver un reCAPTCHA Invisible desde la API necesitas dos cosas que no aparecen en el v2 clásico: enviar el parámetro invisible=1 junto al sitekey y, cuando llega el token, ejecutar la función callback de la página en lugar de rellenar un campo oculto. Ese segundo punto es donde se atasca la mayoría de las integraciones: el token llega correcto, se escribe en g-recaptcha-response y el formulario no avanza, porque nadie disparó el callback.
Abajo tienes el flujo completo, con ejemplos en Python y JavaScript válidos tanto en un script de QA local como en un worker desplegado.
¿Dudas entre invisible y v2 estándar? Revisa las diferencias entre reCAPTCHA v2 y la variante invisible antes de escribir código: el parámetro que envíes depende de eso.
Qué necesitas antes de empezar
- Tu clave API de CaptchaAI, disponible en el panel de control.
- El
sitekey: el valor dedata-sitekeyque cuelga del div o del botón de envío. - La URL completa de la página donde se dispara el desafío.
- Un navegador automatizado (Selenium o Puppeteer). No es opcional si el sitio usa callback.
Ese último punto sorprende a mucha gente: conseguir el token es HTTP puro, pero entregarlo suele exigir una página viva donde ejecutar JavaScript. Si tu arquitectura no contempla un navegador, descúbrelo ahora y no en el despliegue.
Paso 1: confirmar que el widget es invisible
Antes de enviar nada, mira el HTML de la página y busca alguno de estos tres patrones:
<!-- Option 1: div with data-size="invisible" -->
<div class="g-recaptcha" data-sitekey="6LdKlZEU..." data-size="invisible" data-callback="onSubmit"></div>
<!-- Option 2: button with data-sitekey (invisible by default) -->
<button data-sitekey="6LdKlZEU..." data-callback="onSubmit">Submit</button>
<!-- Option 3: programmatic execution -->
<script>
grecaptcha.execute('6LdKlZEU...', {action: 'submit'});
</script>
Cualquiera de las tres firmas confirma que es invisible:
data-size="invisible"en el div del widget.- El
data-sitekeycolgando directamente de un<button>. - Una llamada a
grecaptcha.execute()sin contenedor visible.
Antes de cerrar este paso, apunta dos valores: el sitekey y el contenido de data-callback. El segundo es el que casi todo el mundo olvida, y sin él el paso 4 no tiene a quién llamar.
Paso 2: enviar la tarea a in.php
La llamada es la misma que en v2, con invisible: 1 añadido. Ese parámetro no es opcional.
import requests
response = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6LdKlZEUAAAAAPoxm...",
"pageurl": "https://example.com/signup",
"invisible": 1,
"json": 1
})
task_id = response.json()["request"]
const params = new URLSearchParams({
key: "YOUR_API_KEY", method: "userrecaptcha",
googlekey: "6LdKlZEUAAAAAPoxm...",
pageurl: "https://example.com/signup",
invisible: 1, json: 1,
});
const res = await fetch(`https://ocr.captchaai.com/in.php?${params}`);
const { request: taskId } = await res.json();
La respuesta devuelve el identificador de la tarea en el campo request. Guárdalo: es lo único que necesitas para el sondeo.
Paso 3: consultar el resultado en res.php
CaptchaAI resuelve de forma asíncrona, así que toca sondear. Espera unos segundos entre intentos y trata CAPCHA_NOT_READY como "sigue en cola", no como error:
import time
for _ in range(40):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id, "json": 1
}).json()
if result.get("status") == 1:
token = result["request"]
break
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(f"Error: {result['request']}")
Cualquier otro valor en request sí es un error real —clave inválida, saldo agotado, sitekey incorrecto— y debe cortar el bucle en lugar de agotar los 40 intentos.
Paso 4: inyectar el token ejecutando el callback
Este es el paso que diferencia al invisible del v2 con casilla. El widget no espera un valor en un campo: espera que alguien llame a su función callback pasándole el token.
# Selenium example
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://example.com/signup")
# Find the callback name
callback = driver.execute_script("""
var el = document.querySelector('[data-callback]');
if (el) return el.getAttribute('data-callback');
var btn = document.querySelector('[data-sitekey]');
if (btn) return btn.getAttribute('data-callback');
return null;
""")
# Execute the callback with the token
if callback:
driver.execute_script(f"window['{callback}']('{token}');")
else:
# Fallback: fill hidden field and submit
driver.execute_script(f"""
document.getElementById('g-recaptcha-response').innerHTML = '{token}';
document.querySelector('form').submit();
""")
// Puppeteer example
await page.evaluate((token) => {
const el = document.querySelector('[data-callback]') || document.querySelector('[data-sitekey]');
const callbackName = el?.getAttribute('data-callback');
if (callbackName && window[callbackName]) {
window[callbackName](token);
} else {
document.getElementById('g-recaptcha-response').innerHTML = token;
document.querySelector('form').submit();
}
}, token);
La lógica es idéntica en ambos entornos: leer el nombre del callback desde data-callback, comprobar que existe en window y llamarlo con el token. El bloque else es el plan B para las implementaciones que sí leen el campo oculto.
Cuando el token es correcto y el formulario sigue parado
Es el escenario más frecuente de soporte, y casi nunca es culpa del token. Antes de tocar el código de resolución, descarta por orden:
| Síntoma | Causa habitual | Qué revisar |
|---|---|---|
| El callback se ejecuta y no pasa nada | El nombre leído no es el que usa la página | Imprime data-callback real y compáralo con el que invocas |
window[callback] es undefined |
El script del sitio aún no ha cargado | Espera a que el widget termine de inicializarse antes de inyectar |
| El servidor responde "captcha inválido" | Falta invisible=1 en el envío |
Revisa los parámetros de in.php antes que cualquier otra cosa |
| El formulario se envía pero vuelve con error | Campos obligatorios vacíos o sesión caducada | Rellena todo el formulario antes de disparar el callback |
La regla práctica: un token rechazado apunta al envío; un token aceptado con formulario parado apunta al DOM.
Función completa lista para reutilizar
Envío, sondeo, control de errores y tiempo de espera en una sola función que puedes pegar tal cual en tu proyecto:
import requests
import time
def solve_invisible_recaptcha(api_key, sitekey, page_url):
submit = requests.get("https://ocr.captchaai.com/in.php", params={
"key": api_key, "method": "userrecaptcha", "googlekey": sitekey,
"pageurl": page_url, "invisible": 1, "json": 1
}).json()
if submit.get("status") != 1:
raise RuntimeError(f"Submit error: {submit.get('request')}")
task_id = submit["request"]
for _ in range(40):
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.get("status") == 1:
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(f"Solve error: {result.get('request')}")
raise TimeoutError("Timed out")
token = solve_invisible_recaptcha("YOUR_API_KEY", "6LdKlZEU...", "https://example.com/signup")
Un caso real: QA nocturno de un formulario de alta
Una agencia mantiene el portal de altas de un cliente y verifica cada noche que el registro funciona de principio a fin. El formulario lleva reCAPTCHA Invisible en el botón de envío, así que la suite de Selenium se quedaba bloqueada justo en el último clic. El flujo anterior lo desbloquea contra el staging del propio cliente (staging.example.com/qa-alta): leer el data-sitekey del botón, enviar con invisible=1, sondear el token y ejecutar el callback antes del submit. La misma pauta encaja en cualquier trámite público o portal de cita previa que el equipo tenga autorización para monitorizar; cambia la URL, nunca el patrón. Automatiza solo sitios sobre los que tengas ese derecho, respetando los términos de servicio y la normativa de protección de datos aplicable.
Sobre el coste: CaptchaAI factura por thread concurrente, no por resolución, y eso cambia el cálculo.
- Un script secuencial de QA como este nunca ocupa más de un thread, así que BASIC ($15/mes, 5 threads) le sobra.
- En producción con varios workers en paralelo, el escalón habitual es ADVANCE ($90/mes, 50 threads).
- Lo que dimensiona el plan es la concurrencia máxima, no el total de resoluciones del mes.
Para agencias y freelancers que facturan en monedas volátiles, esa previsibilidad mensual en USD pesa tanto como la cifra.
Preguntas frecuentes
¿Puedo resolverlo sin abrir un navegador?
Depende del sitio:
- Si el formulario lee el campo oculto
g-recaptcha-responsey se envía por POST, te basta con HTTP contrain.phpyres.php. - Si la página depende del callback, necesitas un navegador real donde ejecutar JavaScript.
¿Cuánto tarda normalmente una resolución?
Varía según la carga y el sitio. Por eso el bucle de ejemplo consulta el resultado cada 5 segundos hasta 40 veces: un margen amplio. Ajusta los intentos a tu tiempo de espera, no al revés.
¿Puedo reutilizar un token en varios envíos?
No. Cada token es de un solo uso y caduca en pocos minutos. Resuelve uno por intento de envío; si reintentas el formulario, vuelve a pedir token.
¿Sirve el mismo código para reCAPTCHA Enterprise invisible?
Sí, con un parámetro extra: añade enterprise=1 junto a invisible=1 en la solicitud. El resto del flujo —sondeo e inyección por callback— es idéntico.
Empieza a resolver reCAPTCHA Invisible
Consigue tu clave API en captchaai.com/api.php, añade invisible=1 a tu código v2 y reutiliza el patrón de inyección por callback. Con BASIC ($15/mes, 5 threads) validas la integración completa antes de escalar threads.
¿Prefieres partir de un proyecto ya montado, con entorno, sondeo, reintentos y manejo de errores? Ver el ejemplo ejecutable completo en GitHub →