Explainers

Guía de detección de implementación de Cloudflare Turnstile

Antes de resolver un challenge de Cloudflare Turnstile, hay que detectarlo en la página y extraer el sitekey. Turnstile puede incrustarse mediante atributos HTML, llamadas a la API de JavaScript o cargarse dinámicamente tras renderizar la página. Esta guía cubre todos los métodos de detección, desde el análisis de HTML estático hasta el análisis de JavaScript en tiempo de ejecución.


Métodos de implementación de torniquetes.

Los sitios incorporan Turnstile de tres maneras, cada una de las cuales requiere un enfoque de detección diferente:

Método Cómo funciona Dificultad de detección
HTML implícito <div class="cf-turnstile" data-sitekey="..."> en la fuente de la página Fácil (HTML estático)
JavaScript explícito turnstile.render() llamado en script Medio (analizar JS)
Carga dinámica Widget cargado después de una acción del usuario o XHR Difícil (requiere ejecución JS)

Método 1: detección de HTML estático

La integración de Turnstile más simple utiliza la clase cf-turnstile y el atributo data-sitekey:

import re
import requests

def detect_turnstile_html(url):
    """Detect Turnstile from static HTML."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
        "Accept": "text/html,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(url, headers=headers, timeout=15)
    html = response.text

    result = {
        "turnstile_found": False,
        "sitekey": None,
        "mode": None,
        "theme": None,
        "action": None,
        "script_loaded": False,
    }

    # Check for Turnstile script
    if "challenges.cloudflare.com/turnstile" in html:
        result["script_loaded"] = True

    # Check for widget container
    if "cf-turnstile" in html:
        result["turnstile_found"] = True

        # Extract sitekey
        sitekey_match = re.search(
            r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', html
        )
        if sitekey_match:
            result["sitekey"] = sitekey_match.group(1)

        # Extract mode
        if 'data-size="invisible"' in html:
            result["mode"] = "invisible"
        elif 'data-appearance="interaction-only"' in html:
            result["mode"] = "non-interactive"
        else:
            result["mode"] = "managed"

        # Extract theme
        theme_match = re.search(r'data-theme=["\'](\w+)["\']', html)
        if theme_match:
            result["theme"] = theme_match.group(1)

        # Extract action
        action_match = re.search(r'data-action=["\']([^"\']+)["\']', html)
        if action_match:
            result["action"] = action_match.group(1)

    return result


# Usage
info = detect_turnstile_html("https://staging.example.com/qa-login")
if info["turnstile_found"]:
    print(f"Sitekey: {info['sitekey']}")
    print(f"Mode: {info['mode']}")

Método 2: detección de API de JavaScript

Algunos sitios usan turnstile.render() en lugar de atributos HTML:

import re

def detect_turnstile_js_api(html):
    """Detect Turnstile from JavaScript render calls."""
    patterns = [
        # turnstile.render('#element', {sitekey: '...'})
        r"turnstile\.render\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*\{([^}]+)\}",
        # turnstile.render(element, {sitekey: '...'})
        r"turnstile\.render\s*\([^,]+,\s*\{([^}]+)\}",
    ]

    for pattern in patterns:
        match = re.search(pattern, html, re.DOTALL)
        if match:
            config_text = match.group(match.lastindex)

            # Extract sitekey from config object
            sitekey_match = re.search(
                r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", config_text
            )
            # Extract callback
            callback_match = re.search(
                r"callback\s*:\s*(\w+|function)", config_text
            )
            # Extract action
            action_match = re.search(
                r"action\s*:\s*['\"]([^'\"]+)['\"]", config_text
            )
            # Extract appearance
            appearance_match = re.search(
                r"appearance\s*:\s*['\"]([^'\"]+)['\"]", config_text
            )

            return {
                "found": True,
                "method": "javascript_api",
                "sitekey": sitekey_match.group(1) if sitekey_match else None,
                "callback": callback_match.group(1) if callback_match else None,
                "action": action_match.group(1) if action_match else None,
                "appearance": appearance_match.group(1) if appearance_match else None,
            }

    return {"found": False, "method": None}

Método 3: detección de carga dinámica (Selenium/Puppeteer)

Cuando Turnstile se carga dinámicamente después de la interacción de la página:

Python (Selenium)

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 re

def detect_turnstile_dynamic(url):
    """Detect dynamically loaded Turnstile using Selenium."""
    options = webdriver.ChromeOptions()
    driver = webdriver.Chrome(options=options)

    try:
        driver.get(url)

        # Wait for page to fully load
        WebDriverWait(driver, 10).until(
            lambda d: d.execute_script("return document.readyState") == "complete"
        )

        result = {
            "turnstile_found": False,
            "sitekey": None,
            "iframe_present": False,
            "response_field": False,
        }

        # Check for Turnstile iframe
        iframes = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='challenges.cloudflare.com']")
        if iframes:
            result["turnstile_found"] = True
            result["iframe_present"] = True

        # Check for cf-turnstile container
        containers = driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
        for container in containers:
            sitekey = container.get_attribute("data-sitekey")
            if sitekey:
                result["turnstile_found"] = True
                result["sitekey"] = sitekey

        # Check for hidden response field
        response_fields = driver.find_elements(
            By.CSS_SELECTOR, "[name='cf-turnstile-response'], [name='g-recaptcha-response']"
        )
        if response_fields:
            result["response_field"] = True

        # Check page source for JS API render
        page_source = driver.page_source
        js_match = re.search(
            r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", page_source
        )
        if js_match and not result["sitekey"]:
            result["sitekey"] = js_match.group(1)
            result["turnstile_found"] = True

        return result

    finally:
        driver.quit()

Node.js (Puppeteer)

const puppeteer = require("puppeteer");

async function detectTurnstileDynamic(url) {
  const browser = await puppeteer.launch({
    headless: "new",
    args: [],
  });

  const page = await browser.newPage();

  const result = {
    turnstileFound: false,
    sitekey: null,
    iframePresent: false,
    responseField: false,
    scriptUrl: null,
  };

  // Monitor network for Turnstile script
  page.on("response", (response) => {
    if (response.url().includes("challenges.cloudflare.com/turnstile")) {
      result.scriptUrl = response.url();
    }
  });

  await page.goto(url, { waitUntil: "networkidle2" });

  // Check for Turnstile container
  const sitekey = await page.evaluate(() => {
    const el = document.querySelector(
      ".cf-turnstile, [data-sitekey]"
    );
    return el ? el.getAttribute("data-sitekey") : null;
  });

  if (sitekey) {
    result.turnstileFound = true;
    result.sitekey = sitekey;
  }

  // Check for Turnstile iframe
  const iframes = await page.$$("iframe[src*='challenges.cloudflare.com']");
  if (iframes.length > 0) {
    result.turnstileFound = true;
    result.iframePresent = true;
  }

  // Check for response field
  const responseField = await page.$(
    "[name='cf-turnstile-response']"
  );
  result.responseField = !!responseField;

  await browser.close();
  return result;
}

detectTurnstileDynamic("https://staging.example.com/qa-login").then(console.log);

Clase de detección integral

import re
import requests

class TurnstileDetector:
    """Detect Cloudflare Turnstile across all implementation methods."""

    TURNSTILE_SCRIPT = "challenges.cloudflare.com/turnstile"
    SITEKEY_PATTERNS = [
        r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
        r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"TURNSTILE_SITE_KEY\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
    ]

    def __init__(self, url, html=None):
        self.url = url
        self.html = html
        if not self.html:
            self._fetch()

    def _fetch(self):
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 Chrome/120.0.0.0",
            "Accept": "text/html,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
        }
        response = requests.get(self.url, headers=headers, timeout=15)
        self.html = response.text

    def detect(self):
        """Run all detection methods and return results."""
        return {
            "url": self.url,
            "turnstile_present": self.has_turnstile(),
            "sitekey": self.extract_sitekey(),
            "mode": self.detect_mode(),
            "implementation": self.detect_implementation(),
            "script_loaded": self.has_script(),
            "response_field": self.has_response_field(),
            "action": self.extract_action(),
            "theme": self.extract_theme(),
        }

    def has_turnstile(self):
        return (
            self.has_script()
            or "cf-turnstile" in self.html
            or self.extract_sitekey() is not None
        )

    def has_script(self):
        return self.TURNSTILE_SCRIPT in self.html

    def has_response_field(self):
        return "cf-turnstile-response" in self.html

    def extract_sitekey(self):
        for pattern in self.SITEKEY_PATTERNS:
            match = re.search(pattern, self.html)
            if match:
                return match.group(1)
        return None

    def detect_mode(self):
        if 'data-size="invisible"' in self.html or "size: 'invisible'" in self.html:
            return "invisible"
        if 'data-appearance="interaction-only"' in self.html:
            return "non-interactive"
        if "cf-turnstile" in self.html:
            return "managed"
        return "unknown"

    def detect_implementation(self):
        if "cf-turnstile" in self.html and re.search(r"data-sitekey=", self.html):
            return "html_implicit"
        if "turnstile.render" in self.html:
            return "javascript_explicit"
        if self.has_script() and not "cf-turnstile" in self.html:
            return "dynamic_loading"
        return "unknown"

    def extract_action(self):
        match = re.search(r'data-action=["\']([^"\']+)["\']', self.html)
        if match:
            return match.group(1)
        match = re.search(r"action\s*:\s*['\"]([^'\"]+)['\"]", self.html)
        return match.group(1) if match else None

    def extract_theme(self):
        match = re.search(r'data-theme=["\'](\w+)["\']', self.html)
        return match.group(1) if match else "auto"


# Usage
detector = TurnstileDetector("https://staging.example.com/qa-login")
info = detector.detect()

if info["turnstile_present"]:
    print(f"Sitekey: {info['sitekey']}")
    print(f"Mode: {info['mode']}")
    print(f"Implementation: {info['implementation']}")

Resolviendo después de la detección

Una vez detectado resolver con CaptchaAI:

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_detected_turnstile(detection_result):
    """Solve Turnstile using detection results."""
    if not detection_result["turnstile_present"]:
        raise ValueError("No Turnstile detected")

    if not detection_result["sitekey"]:
        raise ValueError("Sitekey not found — may need browser-based extraction")

    params = {
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": detection_result["sitekey"],
        "pageurl": detection_result["url"],
        "json": 1,
    }

    # Include action if present
    if detection_result.get("action"):
        params["action"] = detection_result["action"]

    submit = requests.post("https://ocr.captchaai.com/in.php", data=params)
    task_id = submit.json()["request"]

    for _ in range(60):
        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"]

    raise TimeoutError("Turnstile solve timed out")


# Full workflow
detector = TurnstileDetector("https://example.com/signup")
info = detector.detect()

if info["turnstile_present"]:
    token = solve_detected_turnstile(info)
    print(f"Token: {token[:50]}...")

Casos extremos

Escenario Desafío Solución
Sitekey en un archivo JS externo No está en el HTML de la página Analizar los archivos JavaScript enlazados para patrones de sitekey
Sitekey desde la respuesta de la API Cargada después de una llamada XHR Monitorizar las solicitudes de red para sitekey en respuestas JSON
Múltiples widgets Turnstile Diferentes sitekeys en la misma página Hacer coincidir el sitekey con el formulario específico que se envía
Turnstile en Shadow DOM No accesible mediante selectores regulares Usar shadowRoot.querySelector en el contexto del navegador
Sitekey renderizada del lado del servidor Incrustada en variables de plantilla Verificar las etiquetas <script> para objetos de configuración
Turnstile tras autenticación No visible en la página pública Primero autenticar y luego detectar

Solución de problemas

Síntoma Causa Solución
Etiqueta de script encontrada pero sin sitekey Renderizado de API JS con configuración de otra fuente Verificar todos los archivos JS enlazados y las respuestas XHR
Se extrajo el sitekey incorrecto Múltiples widgets CAPTCHA en la página Hacer coincidir los sitekeys con los elementos del formulario circundantes
La detección funciona pero la resolución falla Parámetro de acción requerido para la validación Incluir el valor data-action en la solicitud de resolución
El widget no está en el HTML inicial Carga dinámica tras la interacción del usuario Usar Selenium/Puppeteer para renderizar la página completa
Campo cf-turnstile-response vacío El widget aún no se ha completado Esperar a que el widget termine de cargarse

Preguntas frecuentes

¿Pueden cambiar las claves del sitio de Turnstile?

Sí. Los operadores del sitio pueden rotar las claves del sitio en cualquier momento. Extraiga siempre la clave del sitio nueva de la página en lugar de codificarla.

¿Necesito el parámetro de acción?

Sólo si el sitio lo valida del lado del servidor. Si data-action está presente en el HTML, inclúyalo en su solicitud de resolución para obtener mejores resultados.

¿Qué pasa si no puedo encontrar la clave del sitio?

La clave del sitio puede estar en un archivo JavaScript externo, una respuesta API o generarse dinámicamente. Utilice DevTools del navegador (pestaña Red) para encontrarlo, o utilice Selenium/Puppeteer para extraerlo después de renderizar la página completa.

¿El método de detección afecta la resolución?

No. El solucionador de Turnstile de CaptchaAI funciona igual independientemente de cómo se implementó el widget. Solo necesitas el sitekey y la URL de la página.


Resumen

La detección de Cloudflare Turnstile requiere verificar la etiqueta de script, el contenedor cf-turnstile, los atributos data-sitekey y las llamadas turnstile.render(). Usa análisis HTML estático para integraciones simples y Selenium/Puppeteer para widgets cargados dinámicamente. Una vez detectado, resúelvelo con el Solucionador de Turnstile de CaptchaAI usando el sitekey extraído: todos los modos se gestionan de forma idéntica con una tasa de éxito del 100%.

Artículos relacionados

  • Cloudflare Challenge vs Turnstile: cómo detectar
  • Cloudflare Turnstile 403 después del token: solución
  • Extracción del sitekey de Cloudflare Turnstile
Los comentarios están deshabilitados para este artículo.

Publicaciones relacionadas

Troubleshooting Errores y solución de problemas de Cloudflare Turnstile
Errores frecuentes de Cloudflare Turnstile: códigos de error de la API, fallos de validación, token rechazado y cómo corregir cada problema con Captcha AI.

Errores frecuentes de Cloudflare Turnstile: códigos de error de la API, fallos de validación, token rechazado...

Apr 23, 2026
Use Cases Manejo de CAPTCHA del dramaturgo con CaptchaAI
Guía práctica sobre Manejo de CAPTCHA del dramaturgo con Captcha AI con escenarios realistas, consejos de workflow y pasos accionables con Captcha AI.

Guía práctica sobre Manejo de CAPTCHA del dramaturgo con Captcha AI con escenarios realistas, consejos de work...

Apr 26, 2026
Comparisons CAPTCHA de texto vs CAPTCHA de imagen: comparación de desarrolladores
Comparativa práctica de CAPTCHA de texto vs CAPTCHA de imagen: comparación de desarrolladores, centrada en diferencias de costo, precisión, velocidad y esfuerzo...

Comparativa práctica de CAPTCHA de texto vs CAPTCHA de imagen: comparación de desarrolladores, centrada en dif...

Apr 30, 2026