Tutoriales de API

Parámetros de la página Cloudflare Challenge y flujo de tokens

¿Por qué un sitio con Cloudflare devuelve HTTP 503 con "Just a moment..." en vez del contenido? Porque se ha lanzado una página de challenge: un flujo de token que arranca con parámetros ocultos en el HTML, pasa por un proof-of-work en JavaScript y acaba en la cookie qa_validation_cookie, la que autoriza el acceso.

Conocer ese flujo te deja depurar por qué una resolución falla, elegir entre el método cloudflare_challenge y turnstile, y saber cuándo delegar el proceso en un servicio de resolución.

Pasa a diario: un equipo en Madrid o Ciudad de México hace QA de su flujo sobre un portal público con Cloudflare —una cita previa o un marketplace— y ve cada petición cortada por un 503. Respeta siempre los términos de servicio del sitio.

Anatomía de una página de Cloudflare Challenge

Una página de challenge responde con HTTP 503 y trae varios elementos clave:

<!DOCTYPE html>
<html>
<head>
    <title>Just a moment...</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
    <div id="challenge-stage">
        <div id="challenge-body-text">
            Checking if the site connection is secure
        </div>
        <div id="challenge-spinner">
            <!-- Loading spinner -->
        </div>
    </div>

    <div id="challenge-form" style="display:none">
        <form id="challenge-form" action="/..." method="POST">
            <!-- Hidden parameters -->
            <input type="hidden" name="md" value="...">
            <input type="hidden" name="r" value="...">
        </form>
    </div>

    <script src="/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray=...">
    </script>
</body>
</html>

El <script> de /cdn-cgi/challenge-platform/ es el que mueve el flujo.

Parámetros clave del challenge

Los parámetros importantes viven en tres sitios. Primero, los del HTML:

Parámetro Nombre Para qué sirve
ray Ray ID de Cloudflare Identificador único de la solicitud; vincula el challenge con la petición original
md Metadatos del challenge Estado del challenge cifrado
r Token de respuesta Respuesta calculada (la rellena el JavaScript)
chl_opt Opciones del challenge Configuración para el script del challenge
cRay Ray del challenge Ray secundario para el seguimiento del challenge
cZone Zona del challenge ID de zona de Cloudflare
cUPMDTk Marca de tiempo Momento de emisión del challenge
cHash Hash del challenge Validación de integridad

La URL del script codifica la versión y el Ray ID en su ruta:

/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray=ABC123
Componente Significado
/cdn-cgi/challenge-platform/ Infraestructura del challenge de Cloudflare
h/g/ Versión/variante del challenge
orchestrate/ Endpoint de orquestación del challenge
chl_page/v1 Versión de la página del challenge
ray=ABC123 Vínculo con el Ray ID de la solicitud

En ejecución, el script inyecta el objeto _cf_chl_opt:

// Extracted from obfuscated challenge script
window._cf_chl_opt = {
    cvId: '2',           // Challenge version
    cType: 'managed',    // Challenge type
    cNounce: '...',      // Cryptographic nonce
    cRay: '...',         // Challenge Ray ID
    cHash: '...',        // Challenge hash
    cUPMDTk: '...',      // Timestamp
    cFPWv: 'g',          // Fingerprint version
    cTTimeMs: '4000',    // Minimum wait time (ms)
    cTplV: 5,            // Template version
    cLt: '...',          // Challenge lifetime
    cRq: {},             // Challenge request data
};

Al depurar, mira sobre todo cType (managed, interactivo o Turnstile) y cTTimeMs, el tiempo mínimo de espera.

Del challenge a la autorización: el flujo del token

Este es el ciclo completo, desde la primera petición hasta que se sirve el contenido:


1. CLIENT → CLOUDFLARE EDGE
   GET /protected-page
   ↓

2. CLOUDFLARE → CLIENT
   HTTP 503 + Challenge page HTML
   Sets: __cf_bm cookie (bot management tracking)
   Contains: ray ID, challenge script URL
   ↓

3. CLIENT (browser)
   Loads challenge script from /cdn-cgi/challenge-platform/...
   ↓

4. CHALLENGE SCRIPT EXECUTES:
   a. Collects browser fingerprint:

      - Canvas hash
      - WebGL renderer
      - Screen dimensions
      - Installed fonts
      - Timezone
      - Language
   b. Runs proof-of-work:

      - Iterates hash computations
      - Must find answer matching difficulty
   c. Computes timing:

      - Enforces minimum wait (cTTimeMs)
      - Records actual timing
   d. Generates response token:

      - Combines fingerprint + PoW answer + timing
      - Encrypts with challenge nonce
   ↓

5. CLIENT → CLOUDFLARE
   POST /cdn-cgi/challenge-platform/h/g/flow/ov1/...
   Body: { r: "encrypted_response", md: "metadata", ... }
   ↓

6. CLOUDFLARE validates:
   - Proof-of-work answer correct?
   - Timing within acceptable range?
   - Fingerprint consistent with real browser?
   - No replay (nonce check)?
   ↓

7. CLOUDFLARE → CLIENT
   HTTP 200 + Set-Cookie: qa_validation_cookie=...; path=/; expires=...
   + HTTP redirect to original URL
   ↓

8. CLIENT → CLOUDFLARE
   GET /protected-page
   Cookie: qa_validation_cookie=...
   ↓

9. CLOUDFLARE → CLIENT
   HTTP 200 + Protected content

La clave está en el paso 4: saltarse el fingerprint, el proof-of-work o el tiempo mínimo te devuelve al challenge. Las cookies siguen su propio calendario:

Request 1: No cookies
    → Challenge page (503)
    → __cf_bm cookie set

Challenge solve:
    → qa_validation_cookie cookie set

Request 2+: qa_validation_cookie + __cf_bm
    → Content served (200)

After ~30 mins: qa_validation_cookie expires
    → Next request triggers new challenge

Cookies del challenge y sus restricciones

Cloudflare deja varias cookies, pero solo una autoriza el acceso:

Cookie Para qué sirve Duración Alcance
__cf_bm Seguimiento de sesión del bot management 30 min Dominio
qa_validation_cookie Prueba de que superaste el challenge 15 min – 24 h (configurable) Dominio
__cflb Afinidad del balanceador de carga Sesión Dominio
_cfuvid ID de visitante único Sesión Dominio

La qa_validation_cookie no es un token portable: queda atada a la IP que resolvió el challenge, al User-Agent de esa resolución y al dominio que la emitió. Cambiar cualquiera de los tres la invalida:

# ❌ FAILS — IP mismatch
# Solve challenge from IP A, then use qa_validation_cookie from IP B

# ❌ FAILS — UA mismatch
# Solve with Chrome UA, then send requests with Firefox UA

# ✅ WORKS — Same IP + Same UA
session = requests.Session()
session.headers["User-Agent"] = "Mozilla/5.0 ... Chrome/120.0.0.0"
# Use same session for solving and subsequent requests

Regla práctica: resuelve y consume el contenido en la misma sesión y salida de red; cambiarla a mitad del flujo provoca la mayoría de los 403 posteriores.

Cómo extraer los parámetros del challenge

Para ver qué challenge devuelve un sitio, lee los parámetros del HTML del 503, en Python y en Node.js:

import re
import requests

def extract_challenge_params(url):
    """Extract Cloudflare challenge page parameters."""
    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, allow_redirects=False)
    html = response.text

    params = {
        "status_code": response.status_code,
        "cf_ray": response.headers.get("cf-ray", ""),
        "is_challenge": response.status_code == 503,
    }

    if not params["is_challenge"]:
        return params

    # Extract Ray ID from page
    ray_match = re.search(r"ray['\"]?\s*[:=]\s*['\"]([a-f0-9]+)['\"]", html, re.I)
    if ray_match:
        params["ray_id"] = ray_match.group(1)

    # Extract challenge script URL
    script_match = re.search(
        r'src=["\'](/cdn-cgi/challenge-platform/[^"\']+)["\']', html
    )
    if script_match:
        params["challenge_script"] = script_match.group(1)

    # Extract challenge options
    opt_match = re.search(r"_cf_chl_opt\s*=\s*\{([^}]+)\}", html)
    if opt_match:
        opt_text = opt_match.group(1)

        # Parse individual options
        for key in ["cType", "cRay", "cHash", "cTTimeMs", "cvId", "cFPWv"]:
            val_match = re.search(
                rf"{key}\s*:\s*['\"]?([^'\"', }}]+)", opt_text
            )
            if val_match:
                params[key] = val_match.group(1)

    # Extract form parameters
    md_match = re.search(r'name=["\']md["\']\s+value=["\']([^"\']+)["\']', html)
    if md_match:
        params["md"] = md_match.group(1)

    # Extract cookies from response
    params["cookies"] = {
        name: value
        for name, value in response.cookies.items()
    }

    return params


# Usage
params = extract_challenge_params("https://protected-site.com")
if params["is_challenge"]:
    print(f"Challenge type: {params.get('cType', 'unknown')}")
    print(f"Ray ID: {params.get('ray_id', params['cf_ray'])}")
    print(f"Min wait: {params.get('cTTimeMs', '?')}ms")
    print(f"Script: {params.get('challenge_script', 'not found')}")
const axios = require("axios");

async function extractChallengeParams(url) {
  const response = await axios.get(url, {
    headers: {
      "User-Agent": "Mozilla/5.0 Chrome/120.0.0.0",
      Accept: "text/html,*/*;q=0.8",
    },
    validateStatus: () => true,
    maxRedirects: 0,
  });

  const html = response.data;
  const params = {
    statusCode: response.status,
    cfRay: response.headers["cf-ray"] || "",
    isChallenge: response.status === 503,
  };

  if (!params.isChallenge) return params;

  // Extract challenge script URL
  const scriptMatch = html.match(
    /src=["'](\/cdn-cgi\/challenge-platform\/[^"']+)["']/
  );
  if (scriptMatch) params.challengeScript = scriptMatch[1];

  // Extract challenge type
  const typeMatch = html.match(/cType\s*:\s*['"]?(\w+)/);
  if (typeMatch) params.challengeType = typeMatch[1];

  // Extract timing
  const timeMatch = html.match(/cTTimeMs\s*:\s*['"]?(\d+)/);
  if (timeMatch) params.minWaitMs = parseInt(timeMatch[1]);

  return params;
}

extractChallengeParams("https://protected-site.com").then(console.log);

Esto diagnostica, pero no resuelve: el token final solo aparece al ejecutar el JavaScript en un navegador real.

Resolver el Cloudflare Challenge con CaptchaAI

En lugar de reproducir el proof-of-work a mano, envías la URL con el método cloudflare_challenge a in.php y sondeas el resultado en res.php:

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_cloudflare_challenge(target_url):
    """Solve Cloudflare challenge page — CaptchaAI handles token flow."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "cloudflare_challenge",
        "sitekey": "managed",
        "pageurl": target_url,
        "json": 1,
    })

    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("Challenge solve timed out")


# CaptchaAI handles the full flow:
# 1. Loads the challenge page
# 2. Executes JavaScript
# 3. Solves proof-of-work
# 4. Returns clearance token/cookies
token = solve_cloudflare_challenge("https://protected-site.com/login")

CaptchaAI usa navegadores reales, así que no reimplementas el fingerprint ni el temporizador. Los planes van por thread con resoluciones ilimitadas: empieza en BASIC ($15/mes, 5 threads).

Depurar fallos en el challenge

El fallo casi siempre es uno de estos puntos, con su solución habitual:

Punto de fallo Síntoma Causa raíz
La página del challenge no carga Timeout o respuesta vacía Problema de red/proxy
El script no se ejecuta El challenge entra en bucle Faltan APIs de JavaScript
El proof-of-work falla Spinner infinito Timeout de cómputo
Respuesta rechazada Redirige de vuelta al challenge Violación de tiempo o señales del navegador que no cuadran
qa_validation_cookie no se establece Falta la cookie tras resolver Error al parsear la respuesta
qa_validation_cookie rechazada 403 en la siguiente solicitud La IP o la UA no coinciden
Síntoma Causa Solución
El tipo de challenge es "managed" pero la resolución falla El challenge requiere Turnstile, no el challenge de JS Prueba el método turnstile en lugar de cloudflare_challenge
qa_validation_cookie funciona una vez y luego la rechazan La rotación de IP cambió tu IP Fija la IP durante la vida útil del token
La página "Just a moment..." no se resuelve nunca JavaScript bloqueado o mal formado Usa CaptchaAI en lugar de resolver a mano
El challenge reaparece tras cada solicitud No se está enviando qa_validation_cookie Asegúrate de que las cookies persisten en la sesión
Challenges distintos en rutas distintas Reglas WAF por ruta Resuelve cada ruta por separado

Este ayudante señala dónde se rompe el flujo:

def debug_challenge_flow(url, qa_validation_cookie_cookie=None, user_agent=None):
    """Debug the challenge solve flow step by step."""
    ua = user_agent or (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/120.0.0.0"
    )

    steps = []

    # Step 1: Initial request
    response = requests.get(
        url,
        headers={"User-Agent": ua, "Accept": "text/html,*/*;q=0.8"},
        timeout=15,
        allow_redirects=False,
    )
    steps.append({
        "step": "initial_request",
        "status": response.status_code,
        "is_challenge": response.status_code == 503,
        "cf_ray": response.headers.get("cf-ray", ""),
    })

    # Step 2: Test with qa_validation_cookie
    if qa_validation_cookie_cookie:
        session = requests.Session()
        session.cookies.set("qa_validation_cookie", qa_validation_cookie_cookie)
        session.headers["User-Agent"] = ua

        response2 = session.get(url, timeout=15, allow_redirects=False)
        steps.append({
            "step": "with_clearance",
            "status": response2.status_code,
            "passed": response2.status_code == 200,
        })

        if response2.status_code != 200:
            steps.append({
                "step": "diagnosis",
                "issue": "qa_validation_cookie rejected",
                "possible_causes": [
                    "Cookie expired",
                    "IP address changed",
                    "User-Agent mismatch",
                    "Cookie from different domain",
                ],
            })

    return steps

Preguntas frecuentes

__cf_bm es de seguimiento del bot management y no autoriza el acceso. La qa_validation_cookie se emite al superar el challenge y es la que abre el contenido; necesitas ambas en las peticiones siguientes.

¿Uso el método cloudflare_challenge o turnstile?

Depende del tipo. Si ves "Just a moment..." con proof-of-work, es el challenge de JS: usa cloudflare_challenge. Si es un widget con casilla, es Turnstile: usa turnstile.

¿Por qué mi token deja de funcionar al cambiar de servidor o de proxy?

Porque está atada a la IP y al User-Agent que resolvieron el challenge. Enviarla desde otra IP o UA hace que Cloudflare la invalide con un 403. Usa una única sesión de extremo a extremo.

¿Cuánto tiempo puedo reutilizar el token antes de resolver otro challenge?

Lo fija el operador del sitio: por defecto ronda los 30 minutos, en un rango de 15 minutos a 24 horas. Dentro de esa ventana puedes reutilizarlo; al expirar, la siguiente petición lanza un challenge nuevo.

Resumen

El Cloudflare Challenge encadena Ray IDs, el script de /cdn-cgi/challenge-platform/, el objeto _cf_chl_opt y parámetros de formulario en un flujo con proof-of-work que termina en la cookie qa_validation_cookie, atada a la IP y al User-Agent. Con CaptchaAI no parseas nada a mano: el solver gestiona el flujo completo, y conocer los parámetros te indica dónde se rompe al depurar.

Artículos relacionados

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