En un proyecto Django el CAPTCHA aparece en dos frentes que conviene no mezclar, porque cada uno se resuelve con herramientas distintas:
- Tus propios formularios. Tú pones el widget de Turnstile o reCAPTCHA y validas el token en el servidor contra el endpoint oficial de Cloudflare o de Google. CaptchaAI no interviene aquí.
- Sitios ajenos. Cuando tu aplicación tiene que interactuar con un portal externo protegido por CAPTCHA, necesitas resolver el desafío para seguir. Ese es el trabajo de CaptchaAI, con su flujo de envío y sondeo.
Esta guía recorre los dos patrones con código listo para copiar, más las variantes síncrona, asíncrona y con Celery.
Escenario 1: verificar el CAPTCHA en tus formularios Django
- El token que genera el widget en el navegador no vale nada hasta que lo validas en el servidor.
- La comprobación es tuya, no de CaptchaAI: va contra
siteverifyde Cloudflare (o de Google, para reCAPTCHA). - Sin ese paso, cualquiera puede enviar el formulario sin resolver el desafío y saltarse la protección.
Añadir Turnstile a un formulario y validarlo en el servidor
El formulario declara un campo oculto para el token y la vista lo comprueba contra siteverify antes de procesar nada:
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
cf_turnstile_response = forms.CharField(
widget=forms.HiddenInput(),
required=True,
)
# views.py
import requests
from django.conf import settings
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact_view(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
# Verify Turnstile token with Cloudflare
token = form.cleaned_data["cf_turnstile_response"]
verification = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={
"secret": settings.TURNSTILE_SECRET_KEY,
"response": token,
"remoteip": request.META.get("REMOTE_ADDR"),
},
).json()
if verification.get("success"):
# Process the form
return redirect("success")
else:
form.add_error(None, "CAPTCHA verification failed")
else:
form = ContactForm()
return render(request, "contact.html", {
"form": form,
"turnstile_sitekey": settings.TURNSTILE_SITE_KEY,
})
<!-- templates/contact.html -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="cf-turnstile" data-sitekey="{{ turnstile_sitekey }}"></div>
<button type="submit">Send</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Escenario 2: resolver el CAPTCHA de sitios externos con CaptchaAI
Aquí el trabajo lo hace CaptchaAI: tu aplicación consulta un sitio externo protegido por CAPTCHA que no puedes validar tú. Piensa en un panel interno que cada mañana entra a un portal ajeno:
- Un marketplace regional (tipo MercadoLibre o Amazon.es) del que revisas precios.
- Un portal público con un trámite que hay que consultar: una cita previa en España, el SAT en México o AFIP en Argentina.
- Si ese portal levanta un Turnstile, la vista se queda a medias hasta resolver el desafío.
Respeta siempre los términos del sitio y la normativa de protección de datos. El patrón es el mismo: envías la tarea a in.php, sondeas res.php y usas el token antes de que caduque.
Una clase de servicio para hablar con la API de CaptchaAI
Encapsular ese flujo en una sola clase te evita repetir la lógica y te deja un único sitio donde ajustar el timeout o los errores. La clase hace tres cosas:
- Envía la tarea a
in.phpcon el método correcto según el tipo de CAPTCHA. - Sondea
res.phpcada pocos segundos hasta recibir el token o agotar eltimeout. - Lanza una excepción propia (
CaptchaSolveError) si el desafío resulta irresoluble o tarda demasiado.
# services/captcha_solver.py
import time
import requests
from django.conf import settings
class CaptchaSolverService:
"""Django service for solving CAPTCHAs via CaptchaAI."""
API_BASE = "https://ocr.captchaai.com"
def __init__(self):
self.api_key = settings.CAPTCHAAI_API_KEY
def solve_recaptcha_v2(self, sitekey, page_url, invisible=False):
"""Solve reCAPTCHA v2."""
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if invisible:
params["invisible"] = 1
return self._submit_and_poll(params)
def solve_turnstile(self, sitekey, page_url, action=None):
"""Solve Cloudflare Turnstile."""
params = {
"key": self.api_key,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if action:
params["action"] = action
return self._submit_and_poll(params)
def solve_image(self, image_base64):
"""Solve image/text CAPTCHA."""
return self._submit_and_poll({
"key": self.api_key,
"method": "base64",
"body": image_base64,
"json": 1,
})
def get_balance(self):
"""Check API balance."""
response = requests.get(f"{self.API_BASE}/res.php", params={
"key": self.api_key,
"action": "getbalance",
"json": 1,
}, timeout=30)
return float(response.json().get("request", 0))
def _submit_and_poll(self, params, timeout=120):
"""Submit task and poll for result."""
# Submit
response = requests.post(f"{self.API_BASE}/in.php", data=params, timeout=30)
response.raise_for_status()
data = response.json()
if data.get("status") != 1:
raise CaptchaSolveError(f"Submit failed: {data.get('request')}")
task_id = data["request"]
# Poll
start = time.time()
while time.time() - start < timeout:
time.sleep(5)
result = requests.get(f"{self.API_BASE}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=30).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise CaptchaSolveError("CAPTCHA unsolvable")
raise CaptchaSolveError("Solve timed out")
class CaptchaSolveError(Exception):
pass
Configuración en settings.py
Guarda la clave API y las claves de Turnstile fuera del código fuente. En este ejemplo van en settings.py por claridad, pero en producción llegan desde variables de entorno:
# settings.py
CAPTCHAAI_API_KEY = "YOUR_API_KEY"
TURNSTILE_SITE_KEY = "0x4AAAAAAAC3DHQhMMQ_Rxrg"
TURNSTILE_SECRET_KEY = "0x4AAAAAAAC3DHQhYYY_secret"
Llamar al servicio desde tus vistas y comandos
Vista para recopilación de datos externos
La vista recibe la URL objetivo, pide a CaptchaAI el token de Turnstile y lo reenvía al sitio protegido para acceder al recurso:
# views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .services.captcha_solver import CaptchaSolverService, CaptchaSolveError
@require_POST
def scrape_external_data(request):
"""Solve CAPTCHA and fetch data from external CAPTCHA-protected site."""
url = request.POST.get("target_url")
if not url:
return JsonResponse({"error": "target_url required"}, status=400)
solver = CaptchaSolverService()
try:
# Solve the CAPTCHA
token = solver.solve_turnstile(
sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg",
page_url=url,
)
# Use token to access the protected resource
import requests as http_requests
response = http_requests.post(url, data={
"cf-turnstile-response": token,
}, timeout=30)
return JsonResponse({
"status": "success",
"data": response.text[:1000],
})
except CaptchaSolveError as e:
return JsonResponse({"error": str(e)}, status=500)
Comando de gestión para pruebas rápidas
Un comando manage.py es la forma más cómoda de probar la integración sin pasar por el navegador:
- Lo lanzas desde la terminal o desde un cron y ves el token en pantalla.
- Te sirve para comprobar una
sitekeynueva antes de meterla en una vista. - Al final imprime el saldo restante. Ejecútalo indicando tipo,
sitekeyy URL:
# management/commands/solve_captcha.py
from django.core.management.base import BaseCommand
from myapp.services.captcha_solver import CaptchaSolverService
class Command(BaseCommand):
help = "Solve a CAPTCHA and print the token"
def add_arguments(self, parser):
parser.add_argument("--type", choices=["recaptcha", "turnstile"], required=True)
parser.add_argument("--sitekey", required=True)
parser.add_argument("--url", required=True)
def handle(self, *args, **options):
solver = CaptchaSolverService()
self.stdout.write(f"Solving {options['type']} for {options['url']}...")
if options["type"] == "recaptcha":
token = solver.solve_recaptcha_v2(options["sitekey"], options["url"])
else:
token = solver.solve_turnstile(options["sitekey"], options["url"])
self.stdout.write(self.style.SUCCESS(f"Token: {token[:50]}..."))
# Check balance
balance = solver.get_balance()
self.stdout.write(f"Remaining balance: ${balance:.2f}")
python manage.py solve_captcha --type turnstile --sitekey 0x4AAA... --url https://example.com
Vistas asíncronas de Django con CaptchaAI
- Django admite vistas asíncronas desde la versión 4.1, así que si tu proyecto ya usa
async defconviene que la resolución no bloquee el bucle de eventos. - Cambia
requestsporaiohttppara que las llamadas ain.phpyres.phpsean no bloqueantes. - Sondea con
asyncio.sleepen lugar detime.sleep, de modo que el worker atienda otras peticiones mientras espera el token.
# views.py (async)
import aiohttp
import asyncio
from django.http import JsonResponse
CAPTCHAAI_API_KEY = "YOUR_API_KEY"
async def solve_captcha_async(request):
"""Async view for solving CAPTCHAs."""
sitekey = request.GET.get("sitekey")
page_url = request.GET.get("url")
if not sitekey or not page_url:
return JsonResponse({"error": "sitekey and url required"}, status=400)
async with aiohttp.ClientSession() as session:
# Submit
async with session.post("https://ocr.captchaai.com/in.php", data={
"key": CAPTCHAAI_API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}) as resp:
data = await resp.json()
if data.get("status") != 1:
return JsonResponse({"error": data.get("request")}, status=500)
task_id = data["request"]
# Poll
for _ in range(30):
await asyncio.sleep(5)
async with session.get("https://ocr.captchaai.com/res.php", params={
"key": CAPTCHAAI_API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}) as resp:
result = await resp.json()
if result.get("status") == 1:
return JsonResponse({"token": result["request"]})
return JsonResponse({"error": "timeout"}, status=504)
Resolución en segundo plano con Celery
Cuando una resolución puede tardar y no quieres que el usuario espere dentro de la petición HTTP, muévela a una tarea de Celery. El flujo queda repartido en cuatro pasos:
- La vista encola el trabajo y responde al instante con un
task_id. - El worker de Celery resuelve el CAPTCHA en segundo plano.
- Si algo falla,
self.retryreintenta hastamax_retries. - El navegador sondea con el
task_idhasta que el resultado está listo.
# tasks.py
from celery import shared_task
from .services.captcha_solver import CaptchaSolverService, CaptchaSolveError
@shared_task(bind=True, max_retries=2, default_retry_delay=10)
def solve_captcha_task(self, captcha_type, sitekey, page_url):
"""Background CAPTCHA solving with Celery."""
solver = CaptchaSolverService()
try:
if captcha_type == "recaptcha_v2":
token = solver.solve_recaptcha_v2(sitekey, page_url)
elif captcha_type == "turnstile":
token = solver.solve_turnstile(sitekey, page_url)
else:
raise ValueError(f"Unknown type: {captcha_type}")
return {"success": True, "token": token}
except CaptchaSolveError as e:
self.retry(exc=e)
# Usage in views
from .tasks import solve_captcha_task
def start_solve(request):
result = solve_captcha_task.delay("turnstile", "0x4AAA...", "https://example.com")
return JsonResponse({"task_id": result.id})
def check_solve(request, task_id):
from celery.result import AsyncResult
result = AsyncResult(task_id)
if result.ready():
return JsonResponse(result.get())
return JsonResponse({"status": "pending"})
Resolución de problemas frecuentes
Los fallos habituales al integrar CaptchaAI en Django caen casi siempre en estas casillas:
| Síntoma | Causa | Solución |
|---|---|---|
CaptchaSolveError en producción |
La clave API no está en la configuración | Añade CAPTCHAAI_API_KEY a la configuración de Django |
| La tarea de Celery se reintenta sin fin | CAPTCHA irresoluble o sitekey incorrecto | Fija max_retries y valida la entrada antes de encolar |
| La vista asíncrona se congela | Hay código síncrono dentro de una vista async | Usa aiohttp en lugar de requests |
| El token caduca antes de enviar el formulario | La resolución tardó demasiado | Resuelve justo antes de usar el token, no por adelantado |
| Errores de importación en el comando de gestión | El servicio no está registrado en la app | Revisa que la app esté en INSTALLED_APPS |
Preguntas frecuentes
¿Qué plan de CaptchaAI necesito para un proyecto Django?
Depende de cuántos CAPTCHA resuelvas a la vez. CaptchaAI se factura por threads (resoluciones simultáneas), no por CAPTCHA: el plan BASIC cuesta $15/mes con 5 threads y resoluciones ilimitadas, y subes de tramo —STANDARD ($30/mes, 15 threads), ADVANCE ($90/mes, 50 threads)— a medida que crece tu volumen. Un panel interno con una tarea Celery cada vez va sobrado con BASIC.
¿El mismo servicio resuelve reCAPTCHA, Turnstile e imágenes?
Sí. La clase CaptchaSolverService expone un método por tipo y todos comparten el flujo submit/poll. CaptchaAI resuelve reCAPTCHA v2/v3, Cloudflare Turnstile y Challenge, GeeTest v3 e imágenes/OCR; hCaptcha y FunCaptcha no están soportados, así que no cuentes con ellos en tu vista.
¿Debo resolver dentro de la vista o con Celery?
En vistas que responden al navegador, usa Celery para que nadie espere 15 segundos o más frente a una pantalla en blanco. La resolución síncrona directa encaja mejor en comandos de gestión, scripts y tareas programadas, donde nadie está mirando la petición en tiempo real.
¿Cómo evito que el token caduque antes de tiempo?
Resuélvelo justo antes de enviarlo. Los tokens de reCAPTCHA caducan a los 120 segundos y los de Turnstile a los 300; guardarlos en caché no compensa. Si usas Celery, encola la resolución lo más cerca posible del momento en que vas a consumir el token.
Resumen
Las aplicaciones Django encajan con CaptchaAI a través de una clase de servicio que envuelve el flujo submit/poll y se reutiliza en toda la base de código. Resuelve de forma síncrona en comandos de gestión, con vistas asíncronas en Django 4.1+ y con tareas de Celery cuando el trabajo va en segundo plano. El mismo servicio cubre reCAPTCHA, Turnstile e imágenes, así que no necesitas una integración distinta por cada tipo.
Artículos relacionados
- Turnstile devuelve 403 tras validar el token: cómo solucionarlo
- Los modos del widget de Cloudflare Turnstile, explicados