Tutoriales

CAPTCHA en modales emergentes: detectar e inyectar el token

Lo que complica un CAPTCHA en un modal no es resolverlo, sino cuándo aparece. La sitekey no está en el HTML inicial: se genera al abrir el modal —al pulsar "Iniciar sesión", al confirmar un pago o cuando salta un intersticial anti-bot—, y hay poco margen antes de que se cierre o caduque la sesión. En esa ventana tu script encadena cinco pasos:

  1. Activar el modal.
  2. Esperar a que el widget se renderice.
  3. Extraer la sitekey del DOM.
  4. Resolver el CAPTCHA con CaptchaAI.
  5. Inyectar el token y enviar.

Los ejemplos usan Playwright (Python), Puppeteer (JavaScript) y la API de CaptchaAI.

Dónde aparecen los CAPTCHA en modales

El disparador determina cómo detectas la apertura del modal:

Patrón Disparador Desafío
Modal de inicio de sesión Clic en el botón "Iniciar sesión" El CAPTCHA se carga dentro del div superpuesto
Intersticial anti-bot Automático tras un comportamiento sospechoso Un modal a pantalla completa bloquea la página
Confirmación de pago Envío del formulario de pago Aparece un modal de verificación
Diálogo de límite de solicitudes Se detectan demasiadas solicitudes Modal con puerta CAPTCHA
Consentimiento de cookies + CAPTCHA Primera visita CAPTCHA integrado en el diálogo de consentimiento

Un caso frecuente en la región: QA del modal de alta de tu tienda tipo marketplace o del modal de verificación de un portal de cita previa. El CAPTCHA solo se pinta tras una acción.

Sincronización: el punto crítico del CAPTCHA modal

Antes de escribir el controlador, conoce la ventana temporal: de ella dependen casi todos los fallos.

Factor Impacto Mitigación
Cierre automático del modal El modal puede cerrarse antes de terminar la resolución Empieza a resolver en cuanto detectes el CAPTCHA
Caducidad de la sesión durante la resolución La sesión del servidor expira mientras esperas la respuesta Mantén la sesión viva con un heartbeat en segundo plano
Retraso de renderizado del widget en el modal El widget tarda entre 1 y 3 segundos en cargar dentro del modal Espera 2 segundos tras ver el modal antes de extraer la sitekey
Caducidad del token al rellenar el formulario El token expira mientras completas el resto del formulario Resuelve el CAPTCHA al final, después de rellenar los demás campos

Python: controlador de CAPTCHA modal con Playwright

El ciclo detectar → resolver → inyectar vive en handle_modal_captcha: activa el modal, lee la sitekey e inyecta el token.

import requests
import time
from playwright.sync_api import sync_playwright

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
    """Submit and poll a CAPTCHA."""
    params = {
        "key": API_KEY,
        "method": method,
        "json": 1,
    }
    if method == "userrecaptcha":
        params["googlekey"] = sitekey
        params["pageurl"] = pageurl
    elif method == "turnstile":
        params["sitekey"] = sitekey
        params["pageurl"] = pageurl

    resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
    if resp.get("status") != 1:
        raise RuntimeError(f"Submit failed: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(60):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve failed: {poll.get('request')}")

    raise RuntimeError("Timeout")


def detect_modal_captcha(page):
    """
    Detect CAPTCHA inside a visible modal/dialog.
    Returns (sitekey, method) or (None, None).
    """
    return page.evaluate("""
        () => {
            // Find visible modals
            const modalSelectors = [
                '.modal.show',
                '.modal[style*="display: block"]',
                'dialog[open]',
                '[role="dialog"]:not([aria-hidden="true"])',
                '.overlay.visible',
                '.popup.active',
                '[class*="modal"][class*="open"]',
            ];

            let modal = null;
            for (const sel of modalSelectors) {
                const el = document.querySelector(sel);
                if (el && el.offsetParent !== null) {
                    modal = el;
                    break;
                }
            }

            // If no modal found, search entire document
            const searchRoot = modal || document;

            // Check for reCAPTCHA
            const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
            if (recaptcha) {
                return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
            }

            // Check for Turnstile
            const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
            if (turnstile) {
                return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
            }

            // Check for hCaptcha
            const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
            if (hcaptcha) {
                return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
            }

            return null;
        }
    """)


def inject_token_in_modal(page, token, method="userrecaptcha"):
    """Inject token into the CAPTCHA inside the modal."""
    if method == "userrecaptcha":
        page.evaluate("""
            (token) => {
                // Find response textarea (may be inside modal)
                const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
                textareas.forEach(ta => {
                    ta.value = token;
                    ta.style.display = 'block';
                });

                // Trigger callback
                if (typeof ___grecaptcha_cfg !== 'undefined') {
                    Object.values(___grecaptcha_cfg.clients).forEach(client => {
                        Object.values(client).forEach(val => {
                            if (val && typeof val === 'object') {
                                Object.values(val).forEach(v => {
                                    if (v && typeof v.callback === 'function') v.callback(token);
                                });
                            }
                        });
                    });
                }
            }
        """, token)
    elif method == "turnstile":
        page.evaluate("""
            (token) => {
                const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
                inputs.forEach(inp => { inp.value = token; });

                if (typeof window.turnstileCallback === 'function') {
                    window.turnstileCallback(token);
                }
            }
        """, token)


def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
    """
    Full workflow: trigger modal, detect CAPTCHA, solve, inject.
    """
    # Step 1: Trigger the modal if needed
    if trigger_selector:
        print(f"Clicking trigger: {trigger_selector}")
        page.click(trigger_selector)

    # Step 2: Wait for modal to become visible
    print("Waiting for modal...")
    modal_selectors = [
        ".modal.show",
        "dialog[open]",
        '[role="dialog"]:not([aria-hidden="true"])',
        ".popup.active",
    ]

    modal_visible = False
    for selector in modal_selectors:
        try:
            page.wait_for_selector(selector, timeout=timeout)
            modal_visible = True
            print(f"  Modal detected: {selector}")
            break
        except Exception:
            continue

    if not modal_visible:
        print("  No modal detected")
        return None

    # Step 3: Wait for CAPTCHA to render inside modal
    time.sleep(2)  # Brief pause for dynamic CAPTCHA loading

    captcha_info = detect_modal_captcha(page)
    if not captcha_info:
        print("  No CAPTCHA found in modal")
        return None

    sitekey = captcha_info["sitekey"]
    method = captcha_info["method"]
    print(f"  Found {method} CAPTCHA: {sitekey[:20]}...")

    # Step 4: Solve via CaptchaAI
    token = solve_captcha(sitekey, page.url, method)
    print(f"  Solved: {token[:30]}...")

    # Step 5: Inject token
    inject_token_in_modal(page, token, method)
    print("  Token injected")

    return token


def main():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        page.goto("https://example.com")
        page.wait_for_load_state("networkidle")

        # Handle CAPTCHA that appears in login modal
        token = handle_modal_captcha(
            page,
            trigger_selector="button#login-btn",
            timeout=10000,
        )

        if token:
            # Fill form fields inside modal
            page.fill('dialog input[name="email"]', "user@example.com")
            page.fill('dialog input[name="password"]', "password123")

            # Submit modal form
            page.click('dialog button[type="submit"]')
            page.wait_for_load_state("networkidle")

        browser.close()


main()

El time.sleep(2) no es adorno: el widget aparece uno o dos segundos después que el modal; sin él, la detección devuelve None.

JavaScript: controlador modal con Puppeteer

Puppeteer condensa la misma secuencia en waitForModal, detectModalCaptcha y handleModalCaptcha.

const puppeteer = require("puppeteer");

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
  const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
  if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
  else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", pageurl); }

  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function waitForModal(page, timeout = 10000) {
  const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
  for (const sel of selectors) {
    try {
      await page.waitForSelector(sel, { visible: true, timeout });
      return sel;
    } catch {}
  }
  return null;
}

async function detectModalCaptcha(page) {
  return page.evaluate(() => {
    const checks = [
      { sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
      { sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
    ];
    for (const { sel, method } of checks) {
      const el = document.querySelector(sel);
      if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
    }
    return null;
  });
}

async function handleModalCaptcha(page, triggerSelector) {
  // Trigger modal
  if (triggerSelector) await page.click(triggerSelector);

  // Wait for modal
  const modalSel = await waitForModal(page);
  if (!modalSel) { console.log("No modal found"); return null; }
  console.log(`Modal visible: ${modalSel}`);

  // Wait for CAPTCHA render
  await new Promise((r) => setTimeout(r, 2000));

  const info = await detectModalCaptcha(page);
  if (!info) { console.log("No CAPTCHA in modal"); return null; }

  console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
  const token = await solveCaptcha(info.sitekey, page.url(), info.method);
  console.log(`Solved: ${token.substring(0, 30)}...`);

  // Inject token
  await page.evaluate((t, method) => {
    if (method === "userrecaptcha") {
      document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
    } else if (method === "turnstile") {
      document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
    }
  }, token, info.method);

  return token;
}

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto("https://example.com", { waitUntil: "networkidle2" });

  const token = await handleModalCaptcha(page, "button#login-btn");
  if (token) {
    await page.type('dialog input[name="email"]', "user@example.com");
    await page.click('dialog button[type="submit"]');
    await page.waitForNavigation();
  }

  await browser.close();
})();

Resolución de problemas frecuentes

Problema Causa Solución
Detectas el modal pero no encuentras el CAPTCHA El CAPTCHA se carga de forma asíncrona después de abrirse el modal Aumenta el tiempo de espera; usa MutationObserver para detectar la inserción del widget
Inyectas el token pero el modal no se cierra No se dispara la función callback Localiza e invoca el callback del CAPTCHA de forma explícita
El modal se cierra durante la resolución Cierre automático por tiempo de espera Desactiva el temporizador del modal vía JS: clearTimeout() sobre el temporizador
La sitekey cambia en cada apertura El modal genera instancias dinámicas del CAPTCHA Extrae siempre la sitekey nueva del DOM del modal; nunca la guardes en caché
El clic no abre el modal Elemento no interactivo o tapado por una superposición Usa page.dispatchEvent o espera a que el elemento sea clicable

Preguntas frecuentes

¿Qué tipos de CAPTCHA en modal puede resolver CaptchaAI?

Los mismos que en cualquier página: reCAPTCHA v2 y v3, Cloudflare Turnstile y Challenge, GeeTest v3 e imágenes/OCR. El código trae una rama para hCaptcha, pero hCaptcha y FunCaptcha no son compatibles hoy.

¿Cómo detecto un modal que aparece solo, sin ningún clic?

Con un MutationObserver configurado antes de navegar: vigila los nuevos nodos del DOM y, al insertarse el modal, arranca la detección sin disparador manual.

¿Por qué el token caduca antes de enviar el formulario?

Porque tiene una vida útil corta y el reloj empieza en la resolución, no en el envío. Si resuelves al principio y tardas en rellenar el formulario, llegas al submit con el token vencido; por eso conviene resolver al final.

¿Cuánto cuesta resolver CAPTCHA en modales a escala?

CaptchaAI factura por thread concurrente, no por resolución, así que el coste depende de cuántos modales proceses en paralelo. BASIC ($15/mes, 5 threads) cubre volúmenes moderados con resoluciones ilimitadas; ADVANCE ($90/mes, 50 threads) da más margen a mayor escala.

Artículos relacionados


Automatiza la detección de CAPTCHA en modales de principio a fin: obtén tu API key de CaptchaAI y pon en marcha el flujo.

Guías relacionadas:

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