Um projeto Django lida com CAPTCHA em duas direções opostas, e confundir as duas é o erro mais comum:
- Entrada: o CAPTCHA aparece no seu próprio formulário — você verifica o token no servidor para barrar bots.
- Saída: sua aplicação esbarra em CAPTCHA num site de terceiros — você precisa resolver esse desafio para a automação ou a coleta de dados continuar.
A entrada é resolvida pelo provedor de proteção (Cloudflare ou Google); a saída é onde a CaptchaAI entra. Este guia cobre os dois fluxos.
Verificando o CAPTCHA que protege seu formulário Django
Como funciona: o widget roda no navegador e devolve um token; se a view não confirmar esse token no servidor, o campo vira decoração — qualquer bot passa direto.
Turnstile no formulário: campo oculto e verificação no servidor
O formulário abaixo expõe um campo oculto para o token. A view envia esse token ao endpoint siteverify da Cloudflare e só processa o restante se a resposta trouxer success: true.
# 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>
Segurança:
TURNSTILE_SECRET_KEYnunca deve aparecer no navegador nem no controle de versão — ele existe só nas configurações do servidor.
Resolvendo CAPTCHA em sites externos com a API da CaptchaAI
Aqui a situação se inverte: sua aplicação precisa acessar um site de terceiros protegido por CAPTCHA — coletar preços, checar disponibilidade, testar um checkout em staging — e é a CaptchaAI que resolve o desafio e devolve o token.
- Suportado: reCAPTCHA v2/v3, Turnstile e Cloudflare Challenge, GeeTest v3, imagem/OCR, grade de imagens, BLS.
- Em beta: CaptchaFox, Friendly Captcha, Lemin.
- Não suportado: hCaptcha, FunCaptcha; GeeTest v4 segue "em breve".
Dados de titulares brasileiros coletados assim — nome, e-mail, CPF — são dados pessoais desde o primeiro log: a LGPD se aplica ao pipeline de scraping tanto quanto ao banco de produção.
Uma classe de serviço reutilizável para a CaptchaAI
A classe abaixo concentra a lógica de envio e consulta em um só lugar, cobrindo reCAPTCHA v2, Turnstile e CAPTCHA de imagem, com o saldo da conta exposto para acompanhar o consumo.
# 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
O que _submit_and_poll faz:
- Envia a tarefa a
in.phpe recebe umtask_id. - Consulta
res.phpa cada 5 s atéstatusvirar1, ou até o timeout de 120 s.
Onde guardar a chave de API e as sitekeys
CAPTCHAAI_API_KEY— credencial da sua conta CaptchaAI, usada para resolver CAPTCHA em sites externos.TURNSTILE_SITE_KEY— sitekey pública do widget, exposta no template.TURNSTILE_SECRET_KEY— chave secreta, usada só no servidor para validar junto à Cloudflare.
Todas via variáveis de ambiente — nunca fixas no código-fonte.
# settings.py
CAPTCHAAI_API_KEY = "YOUR_API_KEY"
TURNSTILE_SITE_KEY = "0x4AAAAAAAC3DHQhMMQ_Rxrg"
TURNSTILE_SECRET_KEY = "0x4AAAAAAAC3DHQhYYY_secret"
Chamando o serviço dentro do Django
A mesma CaptchaSolverService é acionada de contextos diferentes: view, management command, view assíncrona ou tarefa Celery.
View de coleta de dados protegida por CAPTCHA
Esta view recebe a URL alvo, resolve o Turnstile daquele site e reenvia a requisição com o token — padrão comum em coleta autorizada e testes de QA.
# 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)
Um management command para testar fora do navegador
Um management command valida a integração sem subir o servidor: passe o tipo, a sitekey e a URL, e o comando imprime o token e o saldo restante.
# 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}")
Uso:
python manage.py solve_captcha --type turnstile --sitekey 0x4AAA... --url https://example.com
Views assíncronas com Django 4.1+
Django 4.1 trouxe suporte nativo a views assíncronas, e o fluxo de submit/poll da CaptchaAI se encaixa bem nesse modelo — sem bloquear a requisição enquanto espera a resolução.
Atenção: não use
requestssíncrono dentro de uma viewasync— troque poraiohttp, ou você trava o event loop.
# 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)
Celery: resolução em background sem travar a requisição
Para CAPTCHAs mais lentos — grades de imagem, sites com fila de verificação —, delegar ao Celery evita segurar a requisição até o resultado sair: a view dispara a tarefa e devolve um task_id.
Pré-requisitos: worker Celery separado, um broker de mensagens (Redis é o mais comum) e
max_retriesconfigurado para falhas transitórias.
Cada resolução simultânea consome uma thread do plano — o ADVANCE (US$ 90/mês, 50 threads) sustenta 50 de uma vez antes de enfileirar.
# 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"})
Qual abordagem usar: síncrona, assíncrona ou Celery
As três chamam a mesma classe — o que muda é onde e quando a resolução acontece:
| Abordagem | Quando usar | Observação |
|---|---|---|
Síncrona (requests) |
Management commands e scripts fora do ciclo de request | Simples de depurar, mas bloqueia até terminar |
Assíncrona (aiohttp) |
Views Django 4.1+ com várias requisições simultâneas | Evite requests síncrono no bloco assíncrono |
| Celery em background | Cargas longas, grades de imagem, filas de verificação | Exige worker e broker; libera a requisição |
Regra prática: se o usuário está esperando a resposta, não resolva o CAPTCHA de forma síncrona no request-response — delegue ao Celery e devolva um
task_id.
Perguntas frequentes
hCaptcha e FunCaptcha funcionam com a CaptchaAI?
Não, nenhum dos dois — nem o GeeTest v4, que segue como "em breve". Veja a lista de tipos suportados acima antes de montar seu fluxo.
O CAPTCHAAI_API_KEY e o TURNSTILE_SECRET_KEY são a mesma coisa?
Não. TURNSTILE_SECRET_KEY valida o token do seu formulário junto à Cloudflare; CAPTCHAAI_API_KEY resolve CAPTCHA em sites externos — fluxos independentes.
Onde devo guardar a chave de API da CaptchaAI em produção?
Em variáveis de ambiente, via django-environ — nunca fixa em settings.py nem versionada no Git. Rotacione se vazar em log ou repositório público.
Esse mesmo serviço funciona com Django REST Framework?
Sim. CaptchaSolverService não depende de views tradicionais do Django — chame os mesmos métodos a partir de uma APIView ou de um ViewSet do DRF.
Faz sentido reaproveitar o token resolvido em várias requisições?
Não compensa: reCAPTCHA expira em 120 s e Turnstile em 300 s, então o cache raramente vale a complexidade. Resolva logo antes de usar.
Problemas comuns e como resolver
| Sintoma | Causa | Correção |
|---|---|---|
CaptchaSolveError em produção |
Chave ausente nas configurações | Adicione CAPTCHAAI_API_KEY a settings.py |
| Celery tenta de novo sem parar | CAPTCHA sem solução ou sitekey errada |
Defina max_retries e valide a entrada |
| View assíncrona trava (não responde) | Código síncrono dentro de uma view async |
Troque requests por aiohttp |
| Token expira antes do envio | Resolução mais lenta que a validade do token | Resolva perto do envio, nunca antes |
| Erro de import no management command | Serviço não registrado em INSTALLED_APPS |
Confira o registro do app |
res.php devolve CAPCHA_NOT_READY sempre |
Primeira consulta cedo demais | Aguarde ~5 s antes de tratar como erro |
Resumo
Formulários que só confirmam um Turnstile ou reCAPTCHA próprio verificam o token direto na view, sem envolver a CaptchaAI. Quando o CAPTCHA está em um site de terceiros, a mesma CaptchaSolverService cobre resolução síncrona, assíncrona e em background via Celery. A CaptchaAI entra nesse segundo fluxo com uma única classe de serviço.