Si automatizas un flujo protegido por Cloudflare Turnstile, la pregunta práctica no es cómo resolver el CAPTCHA, sino qué modo de widget tienes delante: Managed (Cloudflare decide), Non-Interactive (solo prueba de trabajo, sin interfaz) o Invisible (se ejecuta en silencio). Cada uno cambia lo que ve el usuario.
La buena noticia: los tres devuelven el mismo token cf-turnstile-response y se resuelven con la misma llamada a la API. La diferencia real está en la detección. Esta guía explica cómo distinguir cada modo en el HTML y resolverlo con CaptchaAI.
¿Qué modo tienes delante? Guía rápida
Inspecciona el HTML y recorre la lista; el primer criterio que se cumpla te da el modo:
- ¿Aparece a veces una casilla o recuadro del widget? Es Managed:
data-sizenormal o compact, sin atributos de modo. - ¿Solo hay un spinner y nunca una casilla? Es Non-Interactive: busca
data-appearance="interaction-only". - ¿No hay nada en el viewport pero el campo
cf-turnstile-responseacaba poblándose? Es Invisible: buscadata-size="invisible"o un contenedor oculto.
Modo Managed (el predeterminado)
El modo Managed deja que Cloudflare decida el challenge según la reputación del visitante:
| Reputación | El widget se representa como |
|---|---|
| Alta confianza | Pase invisible (sin interfaz visible) |
| Confianza media | Casilla de verificación (haz clic para verificar) |
| Baja confianza | Desafío o bloque interactivo |
Es el modo más frecuente y el más variable. Piensa en un equipo de QA de una agencia en Madrid o Bogotá que prueba su flujo de login: el mismo formulario puede mostrar la casilla en pruebas manuales y pasar en silencio cuando el tráfico parece confiable. Conviene volver a detectar el modo en cada ejecución, no asumirlo.
Implementación
<!-- Managed mode (default) -->
<div class="cf-turnstile"
data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg"
data-theme="light">
</div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Cómo detectarlo en el HTML
La señal es una ausencia: hay un cf-turnstile, pero ningún atributo de modo explícito.
- Está la clase o el div
cf-turnstile. - No hay
data-appearance="interaction-only"ni"always". - No hay
data-size="invisible".
def is_managed_mode(html):
"""Check if Turnstile is using managed mode (default)."""
# Managed mode is the default — no explicit mode attribute
has_turnstile = "cf-turnstile" in html
has_explicit_mode = 'data-appearance="interaction-only"' in html or \
'data-appearance="always"' in html or \
'appearance: "interaction-only"' in html
return has_turnstile and not has_explicit_mode
Modo Non-Interactive
El modo Non-Interactive nunca muestra una casilla ni elemento interactivo. Su comportamiento en tres puntos:
- Ejecuta un challenge de prueba de trabajo en segundo plano.
- Solo enseña un spinner mientras trabaja.
- Si no puede completarse sin interacción, falla en vez de escalar a una casilla.
Trátalo como todo-o-nada: sin casilla de reserva, un fallo es definitivo.
Dónde aparece este modo
- Formularios de comentarios y widgets de feedback
- Suscripciones a newsletters
- Acciones de bajo valor con fricción mínima
- Endpoints de API con protección del lado del navegador
Implementación
<!-- Non-interactive mode -->
<div class="cf-turnstile"
data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg"
data-appearance="interaction-only">
</div>
O mediante la API de JavaScript:
turnstile.render('#turnstile-container', {
sitekey: '0x4AAAAAAAC3DHQhMMQ_Rxrg',
appearance: 'interaction-only',
callback: function(token) {
document.getElementById('cf-turnstile-response').value = token;
},
});
Comportamiento
Page loads → Widget initializes
↓
Background proof-of-work runs
↓
Success → Token generated (no visible UI)
OR
Failure → Widget reports error (no fallback to checkbox)
Modo Invisible
El modo Invisible no muestra ningún contenedor en el viewport. Sus rasgos definitorios:
- No hay recuadro ni spinner: nada visible ni pulsable.
- Se ejecuta al cargar la página o al activarse por código.
- Produce un token sin ninguna señal visual.
Implementación
<!-- Invisible mode — container is hidden -->
<div id="turnstile-invisible"
class="cf-turnstile"
data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg"
data-size="invisible">
</div>
O completamente a través de JavaScript:
// Programmatic invisible Turnstile
turnstile.render('#hidden-container', {
sitekey: '0x4AAAAAAAC3DHQhMMQ_Rxrg',
size: 'invisible',
callback: function(token) {
// Token ready — submit form automatically
submitForm(token);
},
'error-callback': function() {
// Challenge failed
console.error('Invisible Turnstile failed');
},
});
Por qué cuesta más detectarlo
El modo Invisible es el más difícil de localizar por dos razones combinadas:
- El contenedor no tiene dimensiones visibles: una inspección superficial del DOM no lo delata.
- A menudo se inyecta por JavaScript tras el render inicial, cuando el HTML estático ya ni lo menciona.
Por eso la detección combina varias señales con distintos niveles de confianza:
import re
def detect_invisible_turnstile(html):
"""Detect invisible Turnstile on a page."""
indicators = {
"script_loaded": "challenges.cloudflare.com/turnstile" in html,
"size_invisible": 'data-size="invisible"' in html or
"size: 'invisible'" in html or
'size: "invisible"' in html,
"api_render_call": "turnstile.render" in html,
"response_field": "cf-turnstile-response" in html,
}
if indicators["script_loaded"] and indicators["size_invisible"]:
return {"mode": "invisible", "confidence": "high"}
elif indicators["script_loaded"] and indicators["api_render_call"]:
return {"mode": "invisible_or_programmatic", "confidence": "medium"}
elif indicators["response_field"]:
return {"mode": "turnstile_present", "confidence": "low"}
return {"mode": "none", "confidence": "high"}
Cómo extraer el sitekey en cualquier modo
El sitekey es el único dato que necesitas. Esta función lo extrae en los tres modos:
import re
def extract_turnstile_sitekey(html):
"""Extract Turnstile sitekey from page HTML (works for all modes)."""
# Pattern 1: data-sitekey attribute in HTML
match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', html)
if match:
return match.group(1)
# Pattern 2: JavaScript render call
match = re.search(r"sitekey:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", html)
if match:
return match.group(1)
# Pattern 3: Turnstile config object
match = re.search(r"siteKey['\"]?\s*[:=]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", html)
if match:
return match.group(1)
return None
Cómo resolver los tres modos con CaptchaAI
Con CaptchaAI, los tres modos se resuelven igual: el modo no cambia la llamada a la API. El flujo es siempre el mismo:
- Extrae el
sitekeydel HTML y toma lapageurlde la página. - Envía la tarea con el método
turnstile. - Sondea el resultado hasta recibir el token
cf-turnstile-response.
Python
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_turnstile(sitekey, page_url):
"""Solve any Turnstile mode — managed, non-interactive, or invisible."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_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("Turnstile solve timed out")
# Use with any mode
token = solve_turnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://staging.example.com/qa-login")
print(f"Token: {token[:50]}...")
Node.js
const axios = require("axios");
const API_KEY = "YOUR_API_KEY";
async function solveTurnstile(sitekey, pageUrl) {
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "turnstile",
sitekey,
pageurl: pageUrl,
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("Turnstile solve timed out");
}
// Same function works for all Turnstile modes
solveTurnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://staging.example.com/qa-login")
.then((token) => console.log("Token:", token.substring(0, 50)));
Como toda la lógica pasa por la API —no por el navegador—, no necesitas un navegador headless solo para el widget.
Los tres modos, lado a lado
Con cada modo ya claro, esta tabla resume las diferencias que importan:
| Característica | Managed | Non-Interactive | Invisible |
|---|---|---|---|
| ¿Widget visible? | A veces | Nunca (solo spinner) | Nunca |
| ¿Contenedor requerido? | Sí | Sí | Sí (oculto) |
| ¿Interacción del usuario? | A veces (casilla) | No | No |
| ¿Challenge de prueba de trabajo? | Sí (puede intensificarse) | Sí (siempre) | Sí (siempre) |
| ¿Casilla alternativa? | Sí | No (falla) | No (falla) |
| Salida de token | cf-turnstile-response |
cf-turnstile-response |
cf-turnstile-response |
| Método CaptchaAI | turnstile |
turnstile |
turnstile |
| Recomendado para | Login, registro | Formularios de baja fricción | Verificación en segundo plano |
En la práctica solo hay una decisión de código: detectar el modo. Resolver con CaptchaAI es idéntico en los tres.
Preguntas frecuentes
¿Qué parámetros necesito para resolver Turnstile, sea cual sea el modo?
Solo dos, idénticos en los tres modos (el método siempre es turnstile):
- El
sitekeydel widget. - La
pageurldonde se renderiza.
Ni el token ni la validación cambian entre modos.
¿El modo Invisible tarda más en resolverse?
No de forma apreciable. El challenge a nivel de API es el mismo en los tres modos; lo que cambia es la experiencia del usuario, no el trabajo del solver. Lo que sí varía es cuánto tardas en encontrar el sitekey.
¿Compact es un cuarto modo?
- No.
data-size="compact"es solo una variante de tamaño del widget. - Salvo que se declare lo contrario, funciona en modo Managed; no lo trates como un modo aparte.
¿Un sitio puede cambiar de modo de forma dinámica?
- Sí. Algunos sitios usan Managed por defecto y cambian a Non-Interactive para páginas o segmentos concretos.
- El sitekey suele mantenerse, pero conviene volver a detectar el modo en cada navegación.
¿Necesito un navegador headless para resolver Turnstile con CaptchaAI?
No. La resolución ocurre por API: envías el sitekey y la URL, sondeas y recibes el token. No hace falta automatizar un navegador solo para el widget.
Solución de problemas
| Síntoma | Causa | Solución |
|---|---|---|
| Token válido pero el formulario lo rechaza | Sitekey incorrecto (distinto del widget visible) | Busca un sitekey renderizado en JavaScript |
| Widget no encontrado en el HTML | Invisible cargado tras el render inicial | Espera a la carga completa y revisa las respuestas XHR |
| Varios widgets Turnstile en la página | Distintos sitekeys por formulario | Empareja el sitekey con el formulario concreto |
data-size="compact" confunde la detección |
Compact es una variante de tamaño, no un modo | Compact usa Managed por defecto |
Atributo data-action presente |
Etiqueta para analítica, no es un modo | Incluye la acción si se valida en el servidor |
| El token caduca antes del envío | Los tokens de Turnstile caducan a los 300 s | Resuelve justo antes de enviar |
Resumen
Los tres modos de Cloudflare Turnstile —Managed, Non-Interactive e Invisible— cambian la experiencia del usuario, pero producen el mismo token cf-turnstile-response y se resuelven igual con el solver de Turnstile de CaptchaAI, con una tasa de éxito alta y estable. Lo que de verdad importa al desarrollador es la detección: Managed deja HTML visible; Invisible exige analizar la página a fondo para localizar el sitekey.
Artículos relacionados
- Cloudflare Challenge frente a Turnstile: cómo distinguirlos
- Cloudflare Turnstile: error 403 después del token
- Cloudflare Turnstile: errores y solución de problemas