Si Cloudflare te devuelve 403 justo después de enviar un token de Turnstile válido, el token casi nunca es el culpable: el problema suele estar en la sesión. La cookie que emite la validación no viaja en la solicitud siguiente, las cabeceras no coinciden con las que usaste al resolver, o el token llegó tarde. Esta guía va causa por causa, con el código para descartar cada una.
Comprobación rápida antes de tocar el código
Estos cinco puntos explican la mayoría de los 403 reportados:
- Usas el mismo objeto de sesión para cargar la página, resolver y enviar el token.
- El token se envía una sola vez y antes de 240 segundos desde que lo recibiste.
- El
User-Agenty la IP de salida no cambian entre resolver y enviar. - Las cabeceras
OriginyRefererapuntan a la URL real de la página protegida. - La página sirve Turnstile y no un challenge completo de Cloudflare.
Las seis causas del 403 con token válido
| Causa | Probabilidad |
|---|---|
| Falta la cookie qa_validation_cookie | Muy común |
| El token expiró | Común |
| Endpoint de envío incorrecto | Común |
| Faltan cabeceras de solicitud | Moderado |
| IP no coincide entre resolver y enviar | Moderado |
| Cloudflare Challenge (no Turnstile) | A veces confundido |
Causa 1: la cookie de validación no viaja en la sesión
Turnstile emite cookies durante la validación. Si la solicitud siguiente sale sin ellas —cliente HTTP nuevo, pool de workers que no comparte estado— Cloudflare responde 403 aunque el token se aceptara un segundo antes. Regla práctica: tras un envío correcto, la credencial que abre la página protegida es la sesión, no el token.
import requests
session = requests.Session()
# Step 1: Load the page to get initial cookies
session.get("https://example.com")
# Step 2: Solve Turnstile
token = solve_turnstile(
api_key="YOUR_API_KEY",
sitekey="TURNSTILE_SITEKEY",
pageurl="https://example.com",
)
# Step 3: Submit token to the validation endpoint
# This sets qa_validation_cookie cookie
resp = session.post("https://example.com/api/verify", data={
"cf-turnstile-response": token,
}, headers={
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://example.com",
"Referer": "https://example.com/",
})
# Step 4: Now make your actual request WITH the session cookies
resp = session.get("https://example.com/protected-page")
print(resp.status_code) # Should be 200 now
Cómo confirmarlo en menos de un minuto
- Busca
Set-Cookieen la respuesta al envío del token. - Imprime
session.cookiesjusto antes de la solicitud protegida. - Descarta que un helper cree un cliente HTTP nuevo en cada llamada.
Causa 2: el token caduca antes de llegar al formulario
CaptchaAI resuelve Turnstile en menos de 10 s y el token dura unos 300 segundos, así que si entre resolver y enviar pasan minutos, el retraso está en tu cola:
- tareas esperando a que se libere un worker;
- reintentos con retroceso exponencial demasiado largos;
- un pool saturado que no vacía la cola.
import time
# Solve
start = time.time()
token = solve_turnstile(...)
solve_time = time.time() - start
# Check if token is still fresh
if solve_time > 240: # > 4 minutes is risky
print("Token may be too old, solving again...")
token = solve_turnstile(...)
# Submit immediately
submit_token(token)
Causa 3: el sitio espera el token en otro campo
No todos los sitios leen cf-turnstile-response desde un campo oculto del formulario. Algunos lo esperan en una cabecera propia y otros dentro de un cuerpo JSON con otro nombre. Si envías el token en el lugar equivocado, el servidor lo trata como si no existiera y responde 403.
# Some sites use a hidden form field
data = {
"cf-turnstile-response": token,
"username": "user",
"password": "pass",
}
# Some sites use a custom header
headers = {
"X-Turnstile-Token": token,
}
# Some sites use JSON body
json_data = {
"turnstileToken": token,
"email": "[email protected]",
}
Cómo averiguar el nombre exacto del campo
- Abre las DevTools del navegador y ve a la pestaña Red.
- Completa el desafío Turnstile a mano.
- Localiza la solicitud de envío del formulario.
- Revisa el cuerpo de esa solicitud y copia el nombre exacto del campo del token.
Causa 4: cabeceras incoherentes entre resolver y enviar
Cloudflare evalúa el conjunto de cabeceras como un todo, así que replica las del navegador y fíjalas a nivel de sesión. Las tres que más 403 provocan:
User-Agent: el mismo que usaste al resolver, sin excepciones.OriginyReferer: la URL real de la página del formulario.AcceptySec-Fetch-*: coherentes con una navegación de navegador real.
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://example.com",
"Referer": "https://staging.example.com/qa-login",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
}
session.headers.update(headers)
Causa 5: no es Turnstile, es Cloudflare Challenge
| Característica | Turnstile | Cloudflare Challenge |
|---|---|---|
| Apariencia | Casilla de verificación visible en la página | Pantalla de challenge de página completa |
| Método CaptchaAI | turnstile |
cloudflare_challenge |
| Campo de token | cf-turnstile-response |
N/A (basado en cookies) |
Son dos sistemas distintos y cada uno se resuelve con su método. Si ves una pantalla completa con un spinner y sin formulario, usa method=cloudflare_challenge: enviar un token que la página nunca pidió produce 403 constantes.
Caso práctico: un portal de trámites que fallaba en la segunda solicitud
Un equipo en Ciudad de México revisaba a diario la disponibilidad de su propio flujo de trámites en un entorno de staging protegido con Turnstile. El envío del token devolvía 200 OK y el HTML correcto; dos minutos después, la consulta al panel interno devolvía 403.
Era la causa 1. El cliente HTTP se creaba dentro de una función auxiliar, así que cada llamada abría una conexión nueva con el almacén de cookies vacío. Al sacar la sesión de la función y pasarla como parámetro, el 403 desapareció sin tocar nada del lado del solver.
El mismo patrón aparece en portales de cita previa en España o en paneles de proveedores en Argentina: flujos donde controlas la cuenta y solo verificas que el formulario sigue vivo. Respeta los términos de servicio y la normativa de protección de datos aplicable.
Diagnóstico rápido de 403 en Turnstile
| Problema | Causa | Solución |
|---|---|---|
| 403 a pesar del token válido | Faltan cookies de sesión | Usa la misma sesión para todas las solicitudes |
| 403 en páginas siguientes | qa_validation_cookie no establecida | La validación del token debe devolver la cookie |
| Funciona una vez, luego 403 | Cookie caducada | Vuelve a resolver para obtener una cookie fresca |
| Siempre 403 | Challenge de página completa, no Turnstile | Usa el método cloudflare_challenge |
Flujo completo de referencia en Python
Este ejemplo une las piezas: extrae el sitekey, envía la tarea a in.php, consulta el resultado en res.php y reenvía el token con la misma sesión.
import requests
import time
import re
def solve_turnstile_and_access(target_url, api_key):
"""Complete flow: solve Turnstile and access protected page."""
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
})
# Load page, get cookies and sitekey
resp = session.get(target_url)
match = re.search(r'data-sitekey="([^"]+)"', resp.text)
if not match:
raise RuntimeError("Turnstile sitekey not found")
sitekey = match.group(1)
# Solve via CaptchaAI
submit_resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": api_key,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": target_url,
"json": 1,
}, timeout=30)
task_id = submit_resp.json()["request"]
# Poll
for _ in range(12):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = poll.json()
if data.get("status") == 1:
token = data["request"]
break
else:
raise TimeoutError("Solve timeout")
# Submit token using the same session
form_resp = session.post(target_url, data={
"cf-turnstile-response": token,
}, headers={
"Origin": f"https://{requests.utils.urlparse(target_url).netloc}",
"Referer": target_url,
})
return session, form_resp
# Usage
session, resp = solve_turnstile_and_access(
"https://staging.example.com/qa-login",
"YOUR_API_KEY",
)
# session now has valid cookies for subsequent requests
Preguntas frecuentes
¿Puedo reutilizar el mismo token de Turnstile en dos solicitudes?
No. El token es de un solo uso: en cuanto el servidor lo valida, queda consumido y un segundo envío devuelve 403. Lo que sí se reutiliza es la cookie que emite esa validación.
¿Cómo distingo un 403 de Cloudflare de un 403 de la aplicación?
Mira la respuesta. Si trae Server: cloudflare y vuelve a cargar el widget, el rechazo es de Cloudflare; si el cuerpo es tu HTML de error, falla el nombre del campo o tu validación.
¿Cuánto tiempo tengo entre resolver y enviar?
Trabaja con un margen de 240 segundos. Si tu cola puede tardar más, resuelve justo antes de enviar.
¿Necesito un proxy para Turnstile?
Normalmente no. Añádelo solo si el sitio verifica la coherencia de la IP y, en ese caso, fíjalo a nivel de sesión para que resolver y enviar salgan por la misma dirección.
¿Qué plan me conviene si mi pipeline reintenta muchos 403?
Depende de cuántas resoluciones simultáneas necesites, no del total mensual. BASIC ($15/mes, 5 threads) cubre un pipeline pequeño; si los reintentos saturan la cola, ADVANCE ($90/mes, 50 threads) da margen sin tocar el código.
Guías relacionadas
Deja de perseguir 403 fantasma: crea tu cuenta en CaptchaAI y resuelve Turnstile desde tu propia sesión.