Resposta curta: você não precisa de navegador. O Turnstile pode ser resolvido em três chamadas HTTP — ler a sitekey na página, enviar a tarefa para a API da CaptchaAI com method=turnstile e consultar o resultado até receber o token. Depois é só mandar esse token no campo cf-turnstile-response junto com o resto do formulário.
Isso muda o custo da automação: um time de QA que testa cadastro em staging.example.com a partir de um worker em São Paulo (AWS sa-east-1) não precisa subir Chrome headless só para atravessar um widget. Se o navegador for inevitável, o caminho é outro — veja o guia de Playwright com Python e CaptchaAI.
O que você precisa antes de começar
pip install requests
Reúna três coisas:
- Uma chave de API da CaptchaAI, criada em captchaai.com — o plano BASIC (US$ 15/mês, 5 threads) já sustenta um pipeline de QA, porque a cobrança é por thread concorrente e não por resolução.
- A URL exata da página que exibe o widget.
- A sitekey (chave pública do widget) do Turnstile.
A sitekey costuma estar no HTML servido, no atributo data-sitekey — um valor público por definição.
Etapa 1: extrair a sitekey do Turnstile na página
Abra a página com cabeçalhos coerentes e procure a sitekey por expressão regular. Três padrões cobrem a maioria das integrações: data-sitekey e as variantes sitekey: / siteKey = de quem inicializa o widget via JavaScript.
import re
import requests
def extract_turnstile_sitekey(url):
"""Extract Cloudflare Turnstile sitekey from page HTML."""
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)
patterns = [
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
]
for pattern in patterns:
match = re.search(pattern, response.text)
if match:
return match.group(1)
return None
sitekey = extract_turnstile_sitekey("https://example.com/signup")
print(f"Sitekey: {sitekey}")
Se a função devolver None, a página provavelmente injeta o widget depois do carregamento — pule para a seção de solução de problemas.
Etapa 2: enviar a tarefa para a API da CaptchaAI
Envie a tarefa para in.php com quatro parâmetros: sua chave, method=turnstile, a sitekey e a pageurl. O json=1 faz a API responder em JSON.
import requests
API_KEY = "YOUR_API_KEY"
def submit_turnstile(sitekey, page_url):
"""Submit Turnstile solving task to CaptchaAI."""
response = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
})
data = response.json()
if data.get("status") != 1:
raise Exception(f"Submit failed: {data.get('request')}")
return data["request"]
task_id = submit_turnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
print(f"Task ID: {task_id}")
A resposta de sucesso traz status: 1 e, em request, o ID da tarefa. Qualquer outro valor é um código de erro que vale tratar já aqui: ERROR_ZERO_BALANCE e ERROR_WRONG_USER_KEY não melhoram com retentativa.
Etapa 3: consultar o resultado até receber o token
A resolução é assíncrona: consulte res.php até o status virar 1. Cinco segundos entre consultas é um bom intervalo para o Turnstile; mais curto só gasta requisição à toa.
import time
def poll_result(task_id, timeout=120):
"""Poll CaptchaAI for the solved Turnstile token."""
start = time.time()
while time.time() - start < timeout:
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"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("Turnstile could not be solved")
raise TimeoutError("Solve timed out")
token = poll_result(task_id)
print(f"Token: {token[:50]}...")
O token retornado tem validade curta e uso único. Trate-o como credencial efêmera: gere, use no envio imediatamente e descarte.
Fluxo completo, do HTML ao envio do formulário
As três etapas em um script só, com uma Session que mantém os cookies entre a leitura da página e o POST final:
import re
import time
import requests
API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://example.com/signup"
def solve_turnstile(sitekey, page_url):
"""Full Turnstile solve: submit + poll."""
# Submit
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
})
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
print(f"Task submitted: {task_id}")
# Poll
for _ in range(30):
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("Solve timed out")
# --- Main flow ---
session = requests.Session()
session.headers.update({
"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",
})
# 1. Get page and extract sitekey
response = session.get(TARGET_URL, timeout=15)
match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text)
if not match:
raise ValueError("Turnstile sitekey not found")
sitekey = match.group(1)
print(f"Sitekey: {sitekey}")
# 2. Solve Turnstile
token = solve_turnstile(sitekey, TARGET_URL)
print(f"Token: {token[:50]}...")
# 3. Submit form with token
form_response = session.post(TARGET_URL, data={
"cf-turnstile-response": token,
"email": "[email protected]",
"password": "SecurePass123",
})
print(f"Form status: {form_response.status_code}")
Repare no ponto crítico do final: o token vai no campo cf-turnstile-response. Esse nome é específico do Turnstile: outros tipos de CAPTCHA usam campos próprios, e trocar um pelo outro é a causa mais comum de "token aceito pela API, formulário rejeitado pelo site".
Quando o widget exige o parâmetro action
Parte das integrações valida no servidor o parâmetro action declarado em data-action. Se ele existe e você não o repassa, o token volta válido, mas é recusado no envio.
def solve_turnstile_with_action(sitekey, page_url, action):
"""Solve Turnstile that requires an action parameter."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"action": action, # Include the action from data-action attribute
"json": 1,
})
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
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("Solve timed out")
Confira o HTML do widget antes de assumir que não há action.
Três padrões de envio do token
A forma como o site consome o token varia — identifique o padrão antes de escrever o POST.
Padrão 1: formulário tradicional com cf-turnstile-response
# Most common — Turnstile uses cf-turnstile-response field
response = session.post(form_url, data={
"cf-turnstile-response": token,
"email": "[email protected]",
})
Padrão 2: API JSON com nome de campo próprio
response = session.post(api_url, json={
"turnstileToken": token,
"email": "[email protected]",
})
Padrão 3: campo renomeado ou duplicado
# Some sites rename the field — check the form HTML
response = session.post(form_url, data={
"cf-turnstile-response": token,
"captcha_token": token, # Custom duplicate field
"action": "signup",
})
Na dúvida, abra o DevTools, envie o formulário uma vez à mão e copie a estrutura exata do payload.
Classe pronta para produção, com retentativa
Em produção você quer retentativa controlada, timeouts explícitos e a decisão de não repetir erros de cobrança ou de chave:
import re
import time
import requests
class TurnstileSolver:
"""Production-ready Turnstile solver with retry logic."""
API_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, max_retries=3):
self.api_key = api_key
self.max_retries = max_retries
def extract_sitekey(self, session, url):
"""Extract Turnstile sitekey from page."""
response = session.get(url, timeout=15)
match = re.search(
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text
)
return match.group(1) if match else None
def solve(self, sitekey, page_url, action=None):
"""Solve Turnstile with retry logic. Returns token string."""
for attempt in range(1, self.max_retries + 1):
try:
token = self._solve_once(sitekey, page_url, action)
return token
except TimeoutError:
print(f"Attempt {attempt} timed out")
except Exception as e:
error_str = str(e)
if "ERROR_ZERO_BALANCE" in error_str:
raise # Don't retry billing errors
if "ERROR_WRONG_USER_KEY" in error_str:
raise
print(f"Attempt {attempt} failed: {e}")
raise Exception(f"Failed after {self.max_retries} attempts")
def _solve_once(self, sitekey, page_url, action=None):
"""Single solve attempt."""
params = {
"key": self.api_key,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if action:
params["action"] = action
submit = requests.post(f"{self.API_URL}/in.php", data=params, timeout=30)
submit.raise_for_status()
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
time.sleep(5)
result = requests.get(f"{self.API_URL}/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 Exception("CAPTCHA unsolvable")
raise TimeoutError("Poll timed out")
# Usage
solver = TurnstileSolver("YOUR_API_KEY")
token = solver.solve("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
Com as 5 threads do BASIC (US$ 15/mês) você mantém cinco instâncias resolvendo em paralelo; subir para STANDARD (US$ 30/mês, 15 threads) é o que aumenta a vazão, já que as resoluções por thread são ilimitadas.
Solução de problemas
| Sintoma | Causa provável | Como corrigir |
|---|---|---|
| Token recebido, mas o formulário rejeita | Sitekey errada ou action ausente |
Extraia a sitekey novamente e inclua o action, se houver |
| "Sitekey não encontrada" | Widget montado via JavaScript | Use Selenium ou Playwright para renderizar a página |
| HTTP 403 antes de ler a página | Requisição sem cabeçalhos de navegador | Envie User-Agent, Accept e Accept-Language coerentes |
| A resolução passa de 60 s | Congestionamento de fila | Normal em horário de pico; aumente o timeout do polling |
| O token funciona uma vez e depois falha | O site exige token novo a cada tentativa | Resolva um token por envio, sem reaproveitar |
Quando a API resolve mas o navegador continua falhando, o problema costuma estar no envio do formulário, não na resolução: compare o payload que o navegador manda com o que o seu script monta.
Onde esse fluxo se aplica
Mantenha o escopo em ambiente próprio: testes de integração em staging, monitoramento autorizado dos endpoints que você opera, validação de formulários antes de um deploy. Se o pipeline tocar dados pessoais reais, considere as obrigações da LGPD (ou do RGPD, em Portugal) sobre o que fica em log e por quanto tempo. Use dados fictícios e URLs de QA como staging.example.com — nunca sites de terceiros como alvo de automação.
Perguntas frequentes
Preciso de navegador para resolver o Turnstile?
Não. Todo o fluxo roda em Python puro com requests. Navegador só é necessário quando a página monta o widget via JavaScript e a sitekey não aparece no HTML servido.
Posso reaproveitar o mesmo token em vários envios?
Não. O token é de uso único e expira rápido. Resolva um por envio: reaproveitar é a causa clássica da falha intermitente que "funciona no primeiro teste".
Quantas threads eu preciso para rodar isso em paralelo?
Cada resolução em andamento ocupa uma thread. BASIC (US$ 15/mês) traz 5, STANDARD (US$ 30/mês) traz 15 e ADVANCE (US$ 90/mês) traz 50, todos com resoluções ilimitadas por thread. Dimensione pelo pico de concorrência, não pelo total mensal.
O modo do widget muda a chamada da API?
Não. Os modos gerenciado, não interativo e invisível usam a mesma chamada com method=turnstile. A diferença é tratada internamente pela CaptchaAI.
E se a página tiver Turnstile e reCAPTCHA ao mesmo tempo?
Identifique qual dos dois protege o formulário e resolva apenas esse. Cada tipo usa um campo de resposta próprio — no Turnstile é sempre cf-turnstile-response —, e o token errado gera rejeição silenciosa. A distinção em relação ao Cloudflare Challenge está em como identificar cada um.
Resumo
São três movimentos: ler a sitekey no HTML, enviar a tarefa à CaptchaAI com method=turnstile e consultar res.php até o token chegar. Envie esse token como cf-turnstile-response dentro da mesma Session que carregou a página. O Turnstile é liberado em menos de 10 s, com alta taxa de sucesso nos tipos suportados, e a cobrança por thread mantém o custo previsível conforme o volume cresce.