La respuesta rápida, en dos casos:
- Te tapa la página entera antes de ver el contenido → es Cloudflare Challenge, y tu método es
cloudflare_challenge. - Aparece como una casilla dentro de un formulario ya cargado → es Turnstile, y tu método es
turnstile.
No hay término medio y no hay autodetección: el método viaja en tu solicitud. Por eso un ERROR_CAPTCHA_UNSOLVABLE repetido casi nunca significa que el desafío sea imposible; significa que estás describiendo una protección que el sitio no tiene.
Señales de un Cloudflare Challenge
El Challenge es un intersticial: lo ves antes de que exista la página real. Suele responder con 403 o 503, y el HTML que recibes no es el del sitio, sino el de Cloudflare.
En el navegador:
- Mensaje del tipo "Comprueba tu navegador antes de acceder…"
- Rueda giratoria de carga de Cloudflare
- El título de la pestaña contiene "Just a moment…"
- La URL no cambia, pero el contenido sí
En el HTML:
<!-- Cloudflare Challenge page indicators -->
<title>Just a moment...</title>
<div id="challenge-running">...</div>
<div class="cf-browser-verification">...</div>
Detección programática:
import requests
resp = requests.get("https://example.com")
is_cf_challenge = (
resp.status_code == 403 and
"Just a moment" in resp.text or
"cf-browser-verification" in resp.text or
"challenge-platform" in resp.text
)
if is_cf_challenge:
print("Cloudflare Challenge detectado — use method: cloudflare_challenge")
Ojo: el marcador del título sigue en inglés aunque el portal esté en español. Cloudflare sirve su intersticial sin localizarlo, así que buscar "Un momento…" no encuentra nada.
Señales de Cloudflare Turnstile
Turnstile vive dentro de una página que ya se cargó por completo, igual que un reCAPTCHA. Si puedes leer el contenido del sitio y solo el botón de enviar está condicionado, es Turnstile.
En el navegador:
- Widget pequeño de Cloudflare dentro de un formulario
- El resto de la página se ve con normalidad
- El widget dice "Verifica que eres humano"
En el HTML:
<!-- Turnstile widget indicators -->
<div class="cf-turnstile" data-sitekey="0x4AAAAAAADnPIDROz1234"></div>
<!-- Or loaded via JavaScript -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>
Detección programática:
import requests
resp = requests.get("https://staging.example.com/qa-login")
is_turnstile = (
"cf-turnstile" in resp.text or
"challenges.cloudflare.com/turnstile" in resp.text
)
if is_turnstile:
# Extract sitekey
if 'data-sitekey="' in resp.text:
start = resp.text.index('data-sitekey="') + 14
end = resp.text.index('"', start)
sitekey = resp.text[start:end]
print(f"Turnstile detected — sitekey: {sitekey}")
print("Use method: turnstile")
Cuidado con los widgets renderizados por JavaScript: si el sitekey no aparece en el HTML que devuelve requests, carga la página con un navegador headless y lee el DOM ya renderizado.
Qué cambia en tu código según cuál sea
Ya sabes cuál tienes delante. Esta es la consecuencia práctica, campo por campo:
| Característica | Cloudflare Challenge | Cloudflare Turnstile |
|---|---|---|
| Tipo | Intersticial de página completa | Widget integrado |
| Apariencia | Bloquea toda la página, "Revisando tu navegador" | Casilla pequeña o widget invisible dentro de un formulario |
| Método CaptchaAI | cloudflare_challenge |
turnstile |
| Devuelve | Cookie qa_validation_cookie |
Cadena de token |
| Requiere proxy | Sí (obligatorio) | Recomendado |
| Requiere User-Agent | Sí (obligatorio) | No |
La diferencia de fondo es la última columna de arriba: qué devuelve cada uno. El Challenge entrega una cookie de sesión atada a la IP que la generó, y de ahí salen las dos casillas obligatorias. Turnstile entrega un token de un solo uso, independiente de tu IP, que inyectas en el formulario y caduca al usarlo.
Una función que decide el método por ti
En producción no quieres inspeccionar cada dominio a mano. Esta función devuelve directamente el método que debes enviar:
def detect_cloudflare_type(url):
"""Detect which Cloudflare protection a URL uses."""
resp = requests.get(url, allow_redirects=True)
# Check for full-page Challenge
if resp.status_code in [403, 503]:
if any(marker in resp.text for marker in [
"Just a moment",
"cf-browser-verification",
"challenge-platform",
"challenge-running"
]):
return {"type": "cloudflare_challenge", "method": "cloudflare_challenge"}
# Check for embedded Turnstile
if "cf-turnstile" in resp.text or "challenges.cloudflare.com/turnstile" in resp.text:
sitekey = None
if 'data-sitekey="' in resp.text:
start = resp.text.index('data-sitekey="') + 14
end = resp.text.index('"', start)
sitekey = resp.text[start:end]
return {"type": "turnstile", "method": "turnstile", "sitekey": sitekey}
return {"type": "none", "method": None}
# Usage
result = detect_cloudflare_type("https://example.com")
print(f"Protection: {result['type']}")
print(f"CaptchaAI method: {result['method']}")
Guarda el resultado en caché por dominio con un TTL corto: las configuraciones de Cloudflare cambian, pero no cada minuto.
Los cuatro errores que explican casi todos los fallos
Antes del código correcto conviene mirar el equivocado, que es probablemente el que ya tienes en producción:
| Error | Resultado | Solución |
|---|---|---|
Usar turnstile en una página de Challenge |
ERROR_CAPTCHA_UNSOLVABLE |
Cambia a cloudflare_challenge |
Usar cloudflare_challenge con Turnstile |
Formato de respuesta incorrecto | Cambia a turnstile |
| Enviar el Challenge sin proxy | ERROR_PROXY_NOT_AUTHORIZED |
Añade el proxy: es obligatorio en el Challenge |
| Enviar Turnstile sin sitekey | ERROR_BAD_PARAMETERS |
Extrae el sitekey del HTML de la página |
Si tu código no aparece aquí, la referencia de códigos de error trae el listado completo.
Cómo se resuelve cada uno
Challenge: cookie, proxy y User-Agent
Los tres campos marcados como obligatorios no lo son por formalidad: sin ellos, la cookie que recibes no sirve desde tu salida de red.
data = {
"key": "YOUR_API_KEY",
"method": "cloudflare_challenge",
"pageurl": "https://example.com",
"proxy": "host:port:user:pass", # MANDATORY
"proxytype": "HTTP", # MANDATORY
"userAgent": "Mozilla/5.0 ...", # MANDATORY
"json": 1
}
# Returns qa_validation_cookie cookie — set it in your session
Turnstile: sitekey y token
Mucho más simple: sitekey y URL de la página. El token vuelve y lo colocas en el campo cf-turnstile-response antes de enviar el formulario.
data = {
"key": "YOUR_API_KEY",
"method": "turnstile",
"sitekey": "0x4AAAAAAADnPIDROz1234",
"pageurl": "https://staging.example.com/qa-login",
"json": 1
}
# Returns a token — inject into cf-turnstile-response field
Ambos métodos consumen threads del mismo plan: no hay recargo por tipo de CAPTCHA. Con BASIC ($15/mes, 5 threads) mantienes cinco resoluciones simultáneas; si recorres varios portales en paralelo, ADVANCE ($90/mes, 50 threads) es el salto habitual.
Un caso concreto: un portal público y un marketplace en el mismo scraper
Un equipo en Ciudad de México vigila dos destinos con el mismo proceso: las citas de un portal de trámites públicos y los precios de su propio catálogo en un marketplace regional. Ambos están detrás de Cloudflare, así que el equipo asume que se resuelven igual. No es así:
- El portal de trámites levanta el intersticial ante cualquier cliente que no parezca un navegador →
cloudflare_challenge, con proxy y User-Agent declarados y la cookie guardada en la sesión. - El marketplace deja ver el catálogo sin fricción y solo pone Turnstile en el acceso al panel de vendedor →
turnstile, sin proxy obligatorio y un token por envío.
Una llamada a detect_cloudflare_type() al inicio de cada ejecución enruta ambos destinos. Y una nota que nunca sobra: revisa los términos de servicio del sitio y la normativa de protección de datos aplicable —GDPR y LOPDGDD en España, LFPDPPP en México— antes de automatizar cualquier recolección.
Preguntas frecuentes
La detección no encuentra el sitekey, pero en el navegador veo el widget. ¿Por qué?
Porque el widget se monta por JavaScript y requests solo ve el HTML inicial. Carga esa URL con un navegador headless y lee el data-sitekey del DOM ya renderizado.
¿Puedo reutilizar el token de Turnstile en varias solicitudes?
No. Es de un solo uso y caduca en pocos minutos: pide uno nuevo por cada envío del formulario.
¿Puede un mismo sitio tener las dos protecciones a la vez?
Sí, y es más frecuente de lo que parece: Challenge en la entrada al dominio y Turnstile en un formulario interno. Resuelve primero el Challenge, conserva la cookie en la sesión y solo entonces pide el token de Turnstile.
¿Sirve este mismo enfoque para reCAPTCHA o GeeTest?
El razonamiento sí; los parámetros no. Cada familia —reCAPTCHA v2, GeeTest v3— tiene sus propios marcadores en el HTML y su propio método, así que el detect_ de arriba se amplía con un marcador por familia. GeeTest v4 aún no está disponible en CaptchaAI; figura como próximamente.
¿Por qué el Challenge necesita proxy y Turnstile no?
Porque la cookie qa_validation_cookie queda vinculada a la IP que la generó. Si se resuelve desde una salida de red distinta a la tuya, el sitio la rechaza. El token de Turnstile, en cambio, es independiente de la IP.
Empieza a resolver Cloudflare hoy
Crea tu cuenta en captchaai.com y ejecuta tu primera detección con el método correcto en la misma sesión.