¿Tu automatización venía funcionando y, de repente, aparece un CAPTCHA en una ruta concreta? Casi siempre la causa no está en tu código, sino en una regla del WAF de Cloudflare que se acaba de activar. Antes de reintentar a ciegas, lee el código de la respuesta:
- 503 con página de challenge → es resoluble; falta ver si es Turnstile o JS.
- 403 con cabecera
cf-ray→ bloqueo duro de la acción Block; ningún solver lo abre. - 200 → la regla dejó pasar la solicitud; no hay nada que resolver.
La acción que devuelve la regla decide el método de CaptchaAI que necesitas.
Qué acciones de una regla WAF acaban en un CAPTCHA
- Managed Challenge e Interactive Challenge → método
turnstile. - JS Challenge → método
cloudflare_challenge. - Block, Allow, Skip y Log → no hay desafío que resolver.
| Acción WAF | Qué ocurre | Estado HTTP | Método CaptchaAI |
|---|---|---|---|
| Managed Challenge | Cloudflare decide: challenge invisible, Turnstile o JS | 503 | turnstile |
| JS Challenge | Página de challenge de JavaScript de 5 segundos | 503 | cloudflare_challenge |
| Interactive Challenge | CAPTCHA tradicional (heredado, obsoleto) | 403 | turnstile |
| Bloquear | 403 duro, sin challenge | 403 | N/A (no resoluble) |
| Permitir | Pasar, sin control | 200 | N/A |
| Saltar | Saltar las reglas WAF restantes | 200 | N/A |
| Registro | Registrar evento, sin acción | 200 | N/A |
Managed Challenge: la acción más habitual
WAF rule matches → Managed Challenge triggered
↓
Cloudflare evaluates visitor:
├─ Low risk → Invisible pass (no visible challenge)
├─ Medium risk → Turnstile widget (click to verify)
└─ High risk → JavaScript challenge page
↓
Successful → qa_validation_cookie cookie issued
Es la que Cloudflare recomienda por defecto y no fija el tipo de desafío de antemano; lo elige según el riesgo de cada visitante:
- Riesgo bajo → pase invisible, sin desafío.
- Riesgo medio → widget de Turnstile.
- Riesgo alto → página de JS Challenge.
Los patrones de regla que más desafían a la automatización
El operador escribe las reglas con el lenguaje de expresiones de Cloudflare; conviene agruparlas por el tipo de señal que miran.
Señales de red: bot score, país y ruta
# Challenge traffic with low bot scores
(cf.bot_management.score lt 30)
→ Action: Managed Challenge
# Challenge non-verified bots
(cf.bot_management.score lt 50 and not cf.bot_management.verified_bot)
→ Action: JS Challenge
- El bot score es el disparador número uno para las herramientas de automatización.
- Los solvers de la API de CaptchaAI reciben puntuaciones de nivel humano porque resuelven en navegadores reales.
# Challenge traffic from specific countries
(ip.geoip.country in {"CN" "RU" "VN" "IN"})
→ Action: Managed Challenge
# Block specific regions entirely
(ip.geoip.country eq "XX")
→ Action: Block
# Challenge login page access
(http.request.uri.path eq "/login" or http.request.uri.path eq "/signup")
→ Action: Managed Challenge
# Challenge API endpoints
(http.request.uri.path contains "/api/")
→ Action: JS Challenge
Señales de la propia solicitud: frecuencia, cabeceras y combinaciones
# Challenge after high request rate
(cf.threat_score gt 10 and http.request.uri.path contains "/search")
→ Action: Managed Challenge
Una frecuencia alta sobre rutas sensibles como /search dispara el challenge.
# Challenge requests with no Accept-Language header
(not http.request.headers["accept-language"])
→ Action: JS Challenge
# Challenge requests with suspicious UA
(http.user_agent contains "python" or http.user_agent contains "curl")
→ Action: Managed Challenge
Cabeceras ausentes o un User-Agent como python o curl delatan a un cliente automatizado.
# Multiple conditions
(cf.bot_management.score lt 30
and http.request.uri.path contains "/api/"
and ip.geoip.country ne "US")
→ Action: JS Challenge
Un caso típico en la región: un equipo que monitoriza citas en un portal público de trámites —una cita previa, la web de un consulado— empieza a recibir un Managed Challenge en /login justo después de que el operador añadiera una regla por país. El código no cambió; cambió la regla. Aplica siempre que trabajes sobre flujos que tienes autorización para automatizar y respetes los términos de servicio del sitio y la normativa de protección de datos aplicable.
Cómo identificar la regla que se activó
Desde fuera no ves las reglas, pero la respuesta HTTP deja pistas suficientes para deducir cuál saltó.
Desde las cabeceras HTTP
import requests
def check_cloudflare_rule_info(url):
"""Extract WAF rule information from Cloudflare challenge response."""
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)
info = {
"status": response.status_code,
"cf_ray": response.headers.get("cf-ray", ""),
"cf_cache_status": response.headers.get("cf-cache-status", ""),
"server": response.headers.get("server", ""),
}
# Challenge-specific info
html = response.text
if response.status_code == 503:
if "jschl" in html:
info["challenge_type"] = "JS Challenge (IUAM or WAF rule)"
elif "challenge-platform" in html:
info["challenge_type"] = "Managed Challenge"
elif "cf-turnstile" in html:
info["challenge_type"] = "Turnstile (Managed Challenge)"
elif response.status_code == 403:
if "cf-ray" in str(response.headers):
info["challenge_type"] = "WAF Block (no challenge)"
else:
info["challenge_type"] = "Origin 403 (not Cloudflare)"
return info
- El código de estado manda: 503 suele ser resoluble; 403 con
cf-ray, bloqueo duro. - El HTML distingue el tipo:
jschles JS Challenge,challenge-platformes Managed ycf-turnstilees Turnstile.
Desde el Ray ID de Cloudflare
Cada respuesta trae una cabecera cf-ray. El operador introduce ese Ray ID en su panel (Security > Events) y ve qué regla saltó; a ti te sirve como referencia para reportar un falso positivo.
Diagnóstico rápido por síntoma
Este cruce entre síntoma y regla probable orienta el diagnóstico antes de escribir código:
| Síntoma | Probable regla WAF | Solución |
|---|---|---|
Challenge solo en /login |
Regla basada en ruta | Resuelve el challenge para esa ruta |
| Challenge solo desde IPs de centros de datos | Bot score o regla de reputación de la IP | Cambia a una salida de red autorizada o resuelve el challenge |
| Challenge varía según el país | Regla basada en país | Usa una salida de red autorizada en un país permitido o resuelve |
| Challenge tras N solicitudes | Regla basada en frecuencia | Reduce la frecuencia de solicitudes o resuelve cada challenge |
| Challenge siempre JS (nunca Turnstile) | Acción JS Challenge (no Managed) | Usa el método cloudflare_challenge |
| 403 sin challenge | Acción Block (no resoluble) | Cambia IP, cabeceras o patrón de solicitud |
Resolver el challenge según la acción de la regla
El método depende solo de lo que se renderiza: turnstile para el widget, cloudflare_challenge para la página de JavaScript.
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_cloudflare_challenge(url, challenge_type):
"""Solve Cloudflare challenge based on WAF rule action."""
if challenge_type == "managed_challenge":
# Managed Challenge typically renders as Turnstile
method = "turnstile"
sitekey = extract_turnstile_sitekey(url)
elif challenge_type == "js_challenge":
# JavaScript Challenge page
method = "cloudflare_challenge"
sitekey = "managed"
else:
raise ValueError(f"Unknown challenge type: {challenge_type}")
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": method,
"sitekey": sitekey,
"pageurl": 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")
def extract_turnstile_sitekey(url):
"""Fetch page and extract Turnstile sitekey."""
import re
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
}
response = requests.get(url, headers=headers, timeout=15)
match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text)
return match.group(1) if match else None
Node.js
const axios = require("axios");
const API_KEY = "YOUR_API_KEY";
async function solveWAFChallenge(url, challengeType) {
const method =
challengeType === "js_challenge" ? "cloudflare_challenge" : "turnstile";
const sitekey =
challengeType === "js_challenge" ? "managed" : await extractSitekey(url);
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method,
sitekey,
pageurl: url,
json: 1,
},
});
const taskId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId, json: 1 },
});
if (result.data.status === 1) {
return result.data.request;
}
}
throw new Error("Challenge solve timed out");
}
async function extractSitekey(url) {
const response = await axios.get(url, {
headers: {
"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0",
},
});
const match = response.data.match(/data-sitekey=["']([0-9x][A-Za-z0-9_-]+)["']/);
return match ? match[1] : null;
}
Cuando el operador cambia las reglas WAF
Las reglas no son estáticas: cada ajuste del operador se nota de inmediato en tu automatización.
| Cambio | Efecto sobre la automatización | Cómo detectar |
|---|---|---|
| Regla agregada | Aparece un nuevo challenge en rutas que funcionaban | Monitorear cambios de estado 503/403 |
| Regla eliminada | El challenge desaparece | 200 donde antes era 503 |
| Acción escalada (Managed → Block) | El challenge resoluble se convierte en bloque duro | 403 en lugar de 503 |
| Acción relajada (Block → Managed) | El bloque duro se convierte en challenge resoluble | 503 con página de challenge |
| Umbral cambiado (bot score 30 → 50) | Más solicitudes challengeadas | Mayor frecuencia de challenge |
| Alcance de ruta cambiado | Diferentes URLs afectadas | Nuevas rutas devuelven challenges |
Monitorizar los cambios de protección
Una comprobación periódica avisa en cuanto una ruta cambia de estado, antes de que se acumulen los fallos:
import requests
import time
def monitor_cloudflare_protection(urls, interval=3600):
"""Monitor protection changes across URLs."""
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",
}
last_status = {}
while True:
for url in urls:
try:
response = requests.get(
url, headers=headers, timeout=15, allow_redirects=False
)
status = response.status_code
has_challenge = status == 503 or "cf-turnstile" in response.text
current = {"status": status, "challenge": has_challenge}
previous = last_status.get(url)
if previous and current != previous:
print(f"[CHANGE] {url}")
print(f" Before: {previous}")
print(f" After: {current}")
last_status[url] = current
except requests.RequestException as e:
print(f"[ERROR] {url}: {e}")
time.sleep(interval)
Preguntas frecuentes
¿Por qué el CAPTCHA aparece de forma intermitente?
Porque Managed Challenge es adaptativo. Cloudflare recalcula el riesgo en cada solicitud, así que la misma ruta puede pasar invisible una vez y exigir un widget de Turnstile a la siguiente. No es un fallo, sino la regla funcionando como se diseñó.
¿Envío el método turnstile o cloudflare_challenge?
Depende de lo que se renderice, no de la regla:
- Widget de Turnstile → método
turnstile. - Página de JavaScript de 5 segundos → método
cloudflare_challenge.
Ante la duda, inspecciona el HTML de la respuesta antes de enviar la tarea.
¿Se puede resolver un bloqueo 403 con un solver?
No. Un 403 de la acción Block no presenta ningún desafío, así que ningún servicio de resolución lo abre. Ahí toca cambiar el patrón de solicitud, las cabeceras o la IP; el solver solo actúa sobre un 503 con página de challenge.
¿Cuánto tiempo sigue válida la sesión tras resolver el challenge?
La cookie qa_validation_cookie deja pasar las solicitudes posteriores sin desafío durante unos 30 minutos y queda vinculada a tu IP y a tu User-Agent; si cambias cualquiera de los dos, el challenge vuelve.
En resumen
- Las reglas WAF disparan el CAPTCHA por bot score, país, ruta, cabeceras o frecuencia de solicitudes.
- Managed Challenge es la acción más común y se renderiza como challenge invisible, Turnstile o JS.
- Lee el código HTTP para decidir: un 503 es resoluble; un 403 de la acción Block, no.
- Resuelve el 503 con CaptchaAI usando
turnstileocloudflare_challengesegún lo que aparezca en pantalla.
Artículos relacionados
- Cloudflare Challenge frente a Turnstile: cómo distinguirlos
- Cómo funciona por dentro Cloudflare Challenge
- Cómo manejar el modo bajo ataque de Cloudflare