¿Buscas el sitekey en el HTML de la página y no aparece por ningún lado, o la API te devuelve ERROR_WRONG_GOOGLEKEY? En la gran mayoría de los casos la causa es la misma: el widget de reCAPTCHA no está en la página principal, sino dentro de uno o varios iframes anidados. El sitekey vive en ese marco interno y el token resuelto hay que devolverlo al contexto exacto donde se renderiza el widget, no a la página de nivel superior.
La solución siempre sigue los mismos cuatro pasos:
- Recorrer el árbol de iframes hasta el marco que renderiza el widget.
- Extraer el sitekey de ese marco.
- Resolver el desafío con CaptchaAI.
- Inyectar el token en el
textareacorrecto y disparar el callback.
En esta guía lo verás con Selenium (Python) y Puppeteer (JavaScript), incluidos los iframes de origen cruzado y con tres o más niveles de anidamiento.
Por qué el CAPTCHA vive dentro de un iframe
Muchos sitios no colocan el reCAPTCHA directamente en su página: lo cargan a través de un componente de terceros que se inyecta en un iframe. Es lo que ocurre en escenarios muy comunes para el desarrollador hispanohablante:
- Pasarelas de pago incrustadas en el checkout de una tienda online.
- Ventanas modales de registro o de inicio de sesión.
- Formularios de portales de cita previa y trámites de administración pública.
El resultado es un widget que se renderiza dos o tres niveles por debajo del documento principal, y ahí es donde empiezan los problemas de extracción. Una estructura anidada típica se ve así:
Main page
└── iframe#payment-frame (cross-origin)
└── iframe[src*="recaptcha/api2/anchor"] (Google-hosted)
└── reCAPTCHA widget
En esta estructura conviene tener claro dónde está cada pieza:
- El sitekey vive en el iframe exterior, el que representa el widget.
- El iframe más interno alojado por Google es solo el desafío visual: no interactúas con él directamente.
- Lo que necesitas es el
data-sitekeydel elemento.g-recaptchao, si no está expuesto, el parámetrok=dentro delsrcdel iframe. Ambos son legibles desde el marco padre.
Errores comunes al resolver CAPTCHA en iframes
Antes de escribir código, conviene tener presentes los fallos que rompen la extracción. Estos cuatro concentran la mayoría de los tickets:
| Error | Qué ocurre | Solución |
|---|---|---|
Usar la URL de la página principal como pageurl |
El sitio rechaza el token | Usa la URL del marco que renderiza el widget CAPTCHA |
| No volver al marco padre tras inyectar | Las acciones siguientes fallan | Llama a switch_to.parent_frame() o switch_to.default_content() después de la inyección |
| Leer el sitekey del iframe equivocado | ERROR_WRONG_GOOGLEKEY |
El sitekey está en el iframe que contiene .g-recaptcha, no en el iframe interno del desafío de Google |
| Ejecutar JS dentro de un iframe cross-origin | SecurityError en la consola |
No puedes usar execute_script dentro de un iframe de origen cruzado; extrae el sitekey del atributo src |
Con el mapa de errores claro, veamos la implementación correcta.
Extraer el sitekey con Selenium (Python)
Selenium exige cambiar de contexto manualmente: switch_to.frame() para entrar en cada iframe y switch_to.default_content() para volver a la raíz. El flujo completo hace lo siguiente:
- Entra en el iframe de pago exterior.
- Localiza el iframe de reCAPTCHA y lee el sitekey de su atributo
src. - Resuelve el desafío con CaptchaAI y espera el token.
- Inyecta el token sin salir del marco y vuelve al contexto principal.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import requests
import re
import time
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example.com/checkout")
# Step 1: Switch into the outer iframe
wait = WebDriverWait(driver, 15)
outer_iframe = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, "iframe#payment-frame"))
)
driver.switch_to.frame(outer_iframe)
# Step 2: Find the reCAPTCHA iframe and extract sitekey
recaptcha_iframe = wait.until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, 'iframe[src*="recaptcha/api2/anchor"]')
)
)
src = recaptcha_iframe.get_attribute("src")
sitekey = re.search(r"k=([A-Za-z0-9_-]+)", src).group(1)
page_url = driver.current_url
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")
# Step 3: Solve con CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1"
})
task_id = resp.json()["request"]
token = None
for _ in range(24):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}).json()
if poll["status"] == 1:
token = poll["request"]
break
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(poll["request"])
print(f"Token: {token[:50]}...")
# Step 4: Inject token (still inside the outer iframe)
driver.execute_script("""
document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
if (typeof grecaptcha !== 'undefined') {
grecaptcha.getResponse = function() { return arguments[0]; };
}
""", token)
# Step 5: Switch back to main page
driver.switch_to.default_content()
print("Token injected, switched back to main frame")
Resultado esperado:
Sitekey: 6Le-SITEKEY-abc123
Page URL: https://example.com/checkout
Token: 03AGdBq26ZfPxL...
Token injected, switched back to main frame
Manejo de marcos con Puppeteer (JavaScript)
Puppeteer trata cada iframe como un objeto Frame de primera clase, así que no hace falta cambiar de contexto manualmente: obtienes un handle del iframe con contentFrame() y trabajas dentro de él directamente. Esto simplifica mucho el anidamiento respecto a Selenium.
const puppeteer = require('puppeteer');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com/checkout', {
waitUntil: 'networkidle2'
});
// Step 1: Find the payment iframe
const paymentFrameHandle = await page.waitForSelector('iframe#payment-frame');
const paymentFrame = await paymentFrameHandle.contentFrame();
// Step 2: Find reCAPTCHA iframe inside the payment frame
const recaptchaFrameHandle = await paymentFrame.waitForSelector(
'iframe[src*="recaptcha/api2/anchor"]'
);
const src = await paymentFrame.evaluate(
el => el.getAttribute('src'), recaptchaFrameHandle
);
const sitekey = src.match(/k=([A-Za-z0-9_-]+)/)[1];
const pageUrl = page.url();
console.log(`Sitekey: ${sitekey}`);
// Step 3: Solve con CaptchaAI
const submitResp = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: {
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageUrl,
json: 1
}
});
const taskId = submitResp.data.request;
let token = null;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const pollResp = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
});
if (pollResp.data.status === 1) {
token = pollResp.data.request;
break;
}
if (pollResp.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(pollResp.data.request);
}
}
console.log(`Token: ${token.substring(0, 50)}...`);
// Step 4: Inject token into the payment frame
await paymentFrame.evaluate((tkn) => {
const textarea = document.querySelector(
'textarea[name="g-recaptcha-response"]'
);
textarea.value = tkn;
const callback = document.querySelector('.g-recaptcha')
?.getAttribute('data-callback');
if (callback && typeof window[callback] === 'function') {
window[callback](tkn);
}
}, token);
console.log('Token injected into payment iframe');
})();
Cómo localizar el marco correcto
Cuando una página tiene varios iframes, conviene recorrerlos antes de resolver nada:
- Selenium: lista todos los marcos con
driver.find_elements(By.TAG_NAME, "iframe")y revisa elsrcde cada uno; el que contienerecaptcha/api2/anchores el del widget. - Puppeteer:
page.frames()te devuelve el árbol completo y puedes filtrar porframe.url().
La regla práctica: el marco que buscas es el que contiene el div con la clase .g-recaptcha, no el iframe interno del desafío de Google.
iframes con anidamiento profundo (tres niveles o más)
Cuando el widget está a tres o más niveles de profundidad, encadena los cambios de marco uno tras otro. La lógica es la misma que con dos niveles, solo que repites el descenso hasta llegar al contexto donde vive el sitekey.
Selenium
# Main → iframe-1 → iframe-2 → CAPTCHA
driver.switch_to.frame(driver.find_element(By.ID, "iframe-1"))
driver.switch_to.frame(driver.find_element(By.ID, "iframe-2"))
# Now extract sitekey from this context
# To go back up one level:
driver.switch_to.parent_frame()
# To go back to main:
driver.switch_to.default_content()
Puppeteer
const frame1 = await (await page.$('iframe#iframe-1')).contentFrame();
const frame2 = await (await frame1.$('iframe#iframe-2')).contentFrame();
// Extract sitekey from frame2
Cuando un marco no carga a tiempo
Un consejo para depurar: si un switch_to.frame() lanza NoSuchFrameException o un waitForSelector se agota, casi siempre es porque el iframe aún no ha terminado de cargar. Espera a que el marco esté presente antes de entrar en él, en lugar de asumir que ya existe.
Preguntas frecuentes
¿Cómo encuentro el iframe correcto cuando la página tiene muchos?
Enumera todos los marcos y filtra por su src. En Selenium usa driver.find_elements(By.TAG_NAME, "iframe") y en Puppeteer page.frames(); el marco del widget es el que contiene recaptcha/api2/anchor en la URL, o el que aloja el div con la clase .g-recaptcha. Descarta el iframe interno de Google, que solo dibuja el desafío.
¿El mismo enfoque sirve para un Cloudflare Turnstile incrustado en un iframe?
Sí. El patrón es idéntico: localiza el marco, lee el sitekey del atributo data-sitekey, resuélvelo con CaptchaAI e inyecta el token de vuelta en el contexto correcto. La única diferencia es el nombre del campo donde se inyecta el token, que cambia según el tipo de CAPTCHA.
¿Por qué falla el envío del formulario aunque el token sea válido?
Depende: casi siempre falta disparar el callback del widget. Muchos sitios no leen el textarea directamente, sino que esperan la función indicada en data-callback. Tras escribir el token, invoca ese callback (como se ve en el ejemplo de Puppeteer) para que la página lo dé por resuelto.
¿Necesito un navegador para extraer el sitekey?
No siempre. Si el sitekey aparece en el HTML inicial que devuelve una petición HTTP, puedes leerlo con una biblioteca de parseo sin abrir un navegador. Pero cuando el widget se carga por JavaScript dentro de un iframe anidado, un navegador controlado con Selenium o Puppeteer es la vía fiable, porque necesitas que los marcos se rendericen antes de leerlos.
Resuelve el CAPTCHA a cualquier profundidad de iframe con CaptchaAI
Ya sea un reCAPTCHA en una pasarela de pago o un Turnstile dentro de una ventana modal, el flujo es el mismo: extrae el sitekey, resuelve con la API e inyecta el token en el marco correcto. Con CaptchaAI trabajas sobre planes basados en threads con resoluciones ilimitadas por thread, desde el plan BASIC ($15/mes, 5 threads), así que puedes escalar tu volumen sin pagar por resolución. Consigue tu clave API en captchaai.com.