Solución de Problemas

Manejo de reCAPTCHA v2 y Cloudflare Turnstile en el mismo sitio

La respuesta corta: no elijas proveedor al escribir el código, detéctalo en cada carga. Un dominio puede servir reCAPTCHA v2 en /login y Cloudflare Turnstile en /checkout, y basta una prueba A/B del equipo de seguridad para que tu script falle sin que tú hayas tocado nada.

El fallo es silencioso: el envío a la API funciona y el thread se consume, pero el formulario rechaza el token porque fue al campo del otro proveedor. Son dos problemas: identificar el widget y enviarlo con su method.

Cómo distinguir reCAPTCHA v2 de Turnstile en el HTML

De cada proveedor necesitas tres datos: la clase CSS del contenedor, el script que carga el widget y el campo del token.

Proveedor Marcador HTML URL del script Campo de respuesta
reCAPTCHA v2 class="g-recaptcha" google.com/recaptcha/api.js g-recaptcha-response
Cloudflare Turnstile class="cf-turnstile" challenges.cloudflare.com/turnstile cf-turnstile-response
hCaptcha class="h-captcha" js.hcaptcha.com/1/api.js h-captcha-response

La fila de hCaptcha está para que sepas reconocerlo: CaptchaAI no resuelve hCaptcha ni FunCaptcha (Arkose Labs). Sí cubre reCAPTCHA v2 —invisible, callback y Enterprise—, reCAPTCHA v3, Turnstile y Cloudflare Challenge, GeeTest v3, imagen/OCR, grid y BLS; CaptchaFox, Friendly Captcha y Lemin están en beta.

Regla práctica: busca primero la clase del proveedor y solo después el data-sitekey. Al revés, cualquier widget con ese atributo da un falso positivo y acabas enviando un sitekey de Turnstile con method=userrecaptcha.

Por qué un mismo dominio mezcla proveedores

Casi nunca es descuido; suele haber una razón operativa.

Escenario Cómo se manifiesta
Páginas distintas, proveedores distintos Login = reCAPTCHA, checkout = Turnstile
Prueba A/B entre proveedores La misma página muestra un tipo u otro al azar
Migración en curso Las páginas antiguas llevan reCAPTCHA, las nuevas llevan Turnstile
Repliegue ante fallo El proveedor principal no responde y entra el secundario
Variación por región reCAPTCHA para visitantes de EE. UU., Turnstile para la UE (GDPR)

La variación por región es la que más despista. Una tienda con presencia en España y México puede servir Turnstile al tráfico europeo —por el encaje con el RGPD y la LOPDGDD suelen preferir un proveedor que no envíe datos a Google— y dejar reCAPTCHA en el resto. Si tu QA corre desde Fráncfort y la de un compañero desde São Paulo, cada uno verá un widget distinto en la misma URL.

Python: detección automática y resolución

import requests
import time
import re
from dataclasses import dataclass

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


@dataclass
class CaptchaInfo:
    provider: str     # "recaptcha" or "turnstile"
    method: str       # API method name
    sitekey: str
    pageurl: str
    response_field: str  # Form field name for the token


def detect_captcha_type(html, pageurl):
    """
    Detect which CAPTCHA provider is on the page.
    Returns CaptchaInfo or None.
    """
    # Check for Turnstile
    turnstile_match = re.search(
        r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
        html,
    )
    if not turnstile_match:
        turnstile_match = re.search(
            r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
            html,
        )

    if turnstile_match:
        return CaptchaInfo(
            provider="turnstile",
            method="turnstile",
            sitekey=turnstile_match.group(1),
            pageurl=pageurl,
            response_field="cf-turnstile-response",
        )

    # Check for reCAPTCHA
    recaptcha_match = re.search(
        r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
        html,
    )
    if not recaptcha_match:
        recaptcha_match = re.search(
            r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
            html,
        )

    # Also check for script-rendered reCAPTCHA
    if not recaptcha_match:
        recaptcha_match = re.search(
            r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
            html,
        )

    if recaptcha_match:
        return CaptchaInfo(
            provider="recaptcha",
            method="userrecaptcha",
            sitekey=recaptcha_match.group(1),
            pageurl=pageurl,
            response_field="g-recaptcha-response",
        )

    return None


def solve_captcha(info):
    """Solve any detected CAPTCHA type via CaptchaAI."""
    params = {
        "key": API_KEY,
        "method": info.method,
        "json": 1,
    }

    if info.method == "userrecaptcha":
        params["googlekey"] = info.sitekey
        params["pageurl"] = info.pageurl
    elif info.method == "turnstile":
        params["sitekey"] = info.sitekey
        params["pageurl"] = info.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 process_page(session, url):
    """Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
    response = session.get(url)
    captcha_info = detect_captcha_type(response.text, url)

    if not captcha_info:
        print(f"No CAPTCHA detected on {url}")
        return None

    print(f"Detected {captcha_info.provider} on {url}")
    print(f"  Sitekey: {captcha_info.sitekey[:30]}...")

    token = solve_captcha(captcha_info)
    print(f"  Solved: {token[:30]}...")

    return {
        "provider": captcha_info.provider,
        "response_field": captcha_info.response_field,
        "token": token,
    }


# Usage: Handle multiple pages with different providers
session = requests.Session()

pages = [
    "https://staging.example.com/qa-login",      # Might have reCAPTCHA
    "https://example.com/checkout",   # Might have Turnstile
]

for url in pages:
    result = process_page(session, url)
    if result:
        form_data = {result["response_field"]: result["token"]}
        # Add other form fields...
        # session.post(url, data=form_data)

El script descarga la página, aplica los patrones y devuelve el proveedor, el method, el sitekey y el campo de destino del token. La resolución es una sola función: no hay dos rutas que mantener, solo un mapeo distinto.

JavaScript: detección dinámica del widget

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

function detectCaptchaType(html, pageurl) {
  // Turnstile
  const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
  if (turnstileMatch) {
    return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
  }

  // reCAPTCHA
  const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
  if (recaptchaMatch) {
    return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
  }

  // Script-rendered reCAPTCHA
  const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
  if (scriptMatch) {
    return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
  }

  return null;
}

async function solveCaptcha(info) {
  const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
  if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
  else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.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 processPage(url) {
  const response = await fetch(url);
  const html = await response.text();
  const info = detectCaptchaType(html, url);

  if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
  console.log(`${info.provider} detected on ${url}`);

  const token = await solveCaptcha(info);
  return { provider: info.provider, field: info.field, token };
}

// Usage
const pages = ["https://staging.example.com/qa-login", "https://example.com/checkout"];
for (const url of pages) {
  const result = await processPage(url);
  if (result) {
    console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
  }
}

La versión en JavaScript añade un tercer intento para reCAPTCHA renderizado por script, cuando el HTML no trae el div con la clase. Es habitual en aplicaciones de una sola página: el marcador aparece tras ejecutarse el bundle.

Errores frecuentes y cómo corregirlos

Síntoma Causa probable Corrección
Se detecta el tipo de CAPTCHA equivocado El patrón coincide con otro elemento Comprueba primero la clase del proveedor, no solo data-sitekey
El token se rechaza pese a resolverse bien Campo de respuesta incorrecto Empareja el campo con el proveedor: g-recaptcha-response frente a cf-turnstile-response
El tipo cambia entre una visita y otra Prueba A/B o selección por geografía Detecta en tiempo de ejecución; nunca fijes el proveedor en el código
Se detectan los dos proveedores Uno está oculto o inactivo Comprueba la visibilidad y resuelve solo el CAPTCHA visible
Falla en widgets renderizados por script No hay marcador en el HTML de origen Busca grecaptcha.render() o turnstile.render() en los scripts

Hay un sexto caso que no es error de código: el sitekey rota. Si la detección funciona pero un dominio rechaza todos los tokens, relee el HTML y compara el sitekey con el que guardaste. Cachear el sitekey junto al proveedor es la trampa más común aquí.

Qué implica para tu plan y tus threads

Detectar dos proveedores no significa pagar dos veces. CaptchaAI factura por thread concurrente —una tarea en vuelo—, no por resolución: reCAPTCHA v2 y Turnstile consumen la misma capacidad, sin recargos por tipo y con resoluciones ilimitadas al mes.

BASIC ($15/mes, 5 threads) cubre la QA de un checkout pequeño o el trabajo de un freelance; STANDARD ($30/mes, 15 threads) y ADVANCE ($90/mes, 50 threads) son los tramos de una agencia con varios dominios. Por encima: PREMIUM ($170/mes, 100 threads), CORPORATE ($240/mes, 150 threads), ENTERPRISE ($300/mes, 200 threads), VIP-1 ($1,500/mes, 1.000 threads), VIP-2 ($4,500/mes, 3.000 threads) y VIP-3 ($7,500/mes, 5.000 threads).

En equipos que facturan en pesos o soles, lo que decide la compra no es el precio sino el costo mensual fijo en USD: con threads sabes lo que gastas aunque el sitio sirva el doble de desafíos. Con pago por resolución, una prueba A/B ajena te cambia la factura.

En portales públicos —trámites del SAT, citas previas españolas, portales BLS de visado— el volumen es irregular: picos cortos y horas planas. Ahí rinde más un plan pequeño con reintentos espaciados que threads ociosos. Respeta los términos de servicio y la normativa de protección de datos aplicable.

Preguntas frecuentes

¿Necesito dos claves API para resolver reCAPTCHA v2 y Turnstile?

No. Una sola clave API cubre ambos. Solo cambian el valor de method y el nombre del parámetro del sitekey: googlekey para reCAPTCHA, sitekey para Turnstile.

¿Puedo cachear el proveedor detectado para ahorrar una petición?

Solo dentro de una sesión y asumiendo el riesgo. Con pruebas A/B o selección por región, la caché te dará el widget equivocado en cuanto rote. Volver a detectar cuesta una lectura del HTML que ya descargas.

¿Sirve este patrón para GeeTest v3 o Cloudflare Challenge?

Sí: añades un patrón más y una rama en el mapeo. GeeTest v3 y Cloudflare Challenge son compatibles. GeeTest v4 todavía no está disponible; figura como próximamente.

¿Qué hago si el sitio muestra hCaptcha en alguna página?

Reconócelo por la clase h-captcha y trátalo aparte: CaptchaAI no resuelve hCaptcha por ahora. Que la detección lo identifique y devuelva un estado explícito, en vez de enviarlo como reCAPTCHA y quemar reintentos en balde.

¿Cambia el tiempo de resolución al alternar entre los dos tipos?

Cada tipo mantiene su propio tiempo de resolución y ninguno penaliza al otro: los threads son genéricos. Ajusta el sondeo para no consultar res.php antes de que la tarea pueda completarse.

Artículos relacionados

Próximos pasos

Deja de mantener dos rutas de código: obtén tu clave API de CaptchaAI y monta la detección una sola vez.

Guías relacionadas:

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