Quando o CAPTCHA aparece no meio de uma coleta, a resposta cabe em quatro passos: leia a assinatura do widget no HTML, envie os parâmetros dele para a API da CaptchaAI, aguarde o token e reenvie a requisição com esse token no campo certo do formulário.
O que costuma faltar não é a chamada da API, é o desenho em volta dela: qual desafio a página usa, quantas resoluções simultâneas o job precisa e o que fazer quando o site recusa um token aparentemente correto. Este guia trata dos três pontos em Python.
Escopo autorizado e LGPD antes do primeiro requests.get
Os padrões deste guia pressupõem coleta autorizada: staging próprio, endpoints internos, monitoramento com permissão do dono do site, testes de integração.
Se os dados incluírem informação pessoal, a LGPD se aplica mesmo com a página pública — base legal, finalidade e prazo de retenção definidos antes da primeira requisição, e o mesmo vale sob o RGPD em Portugal. Na prática vira decisão de código: o que você grava no log e por quanto tempo guarda o HTML bruto.
Como descobrir qual CAPTCHA a página está usando
O diagnóstico começa no HTML. Cada tipo deixa uma assinatura e mapeia para um method da API:
| CAPTCHA | Assinatura no HTML | Método CaptchaAI |
|---|---|---|
| reCAPTCHA v2 | div.g-recaptcha + data-sitekey |
method=userrecaptcha |
| reCAPTCHA v3 | recaptcha/api.js?render= |
method=userrecaptcha&version=v3 |
| Cloudflare Turnstile | div.cf-turnstile |
method=turnstile |
| Cloudflare Challenge | interstício de página inteira | method=turnstile_staging (QA) |
| Imagem/OCR | <img> em formulários legados |
method=base64 |
| GeeTest v3 | gt.js + gt / challenge |
method=geetest |
Duas ressalvas evitam retrabalho. hCaptcha e FunCaptcha (Arkose Labs) não são suportados pela CaptchaAI — se o HTML trouxer h-captcha, este fluxo não se aplica. E o GeeTest v4 ainda não está disponível (anunciado como em breve); só o v3 é resolvido. CaptchaFox, Friendly Captcha e Lemin entram no planejamento como beta.
Dimensionando as threads antes de escrever o loop
A CaptchaAI cobra por thread concorrente, com resoluções ilimitadas no plano. O número que importa não é o total mensal de páginas: é quantas resoluções em andamento ao mesmo tempo.
Junte isso ao tempo de resolução por tipo — Turnstile em menos de 10 s, Cloudflare Challenge em menos de 15 s, reCAPTCHA v2 em menos de 60 s. Um exemplo: uma equipe de QA em São Paulo roda workers em sa-east-1 e dispara 20 páginas em paralelo; se todas exibirem reCAPTCHA v2, cada resolução fica pendente por perto de um minuto e o BASIC (US$ 15/mês, 5 threads) satura na primeira rodada. Aí o ADVANCE (US$ 90/mês, 50 threads) é o degrau natural; só com Turnstile, o STANDARD (US$ 30/mês, 15 threads) cobre com folga.
Essa conta evita o diagnóstico errado mais comum: culpar a API quando o gargalo é a fila de threads do plano.
Padrão 1: detectar e resolver sob demanda
É o padrão mais robusto para páginas mistas: o coletor navega normalmente e só chama a API quando encontra um widget, sem gastar thread em página que passou limpa.
import requests
import time
from bs4 import BeautifulSoup
API_KEY = "YOUR_API_KEY"
class ProtectedScraper:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def scrape(self, url):
resp = self.session.get(url)
# Check for CAPTCHA
if self._has_captcha(resp.text):
resp = self._handle_captcha(resp.text, url)
return resp.text
def _has_captcha(self, html):
indicators = ["g-recaptcha", "cf-turnstile", "h-captcha", "captcha"]
return any(ind in html.lower() for ind in indicators)
def _handle_captcha(self, html, url):
soup = BeautifulSoup(html, "html.parser")
# reCAPTCHA v2
rc = soup.find("div", class_="g-recaptcha")
if rc:
token = self._solve_recaptcha(rc["data-sitekey"], url)
return self.session.post(url, data={"g-recaptcha-response": token})
# Cloudflare Turnstile
ts = soup.find("div", class_="cf-turnstile")
if ts:
token = self._solve_turnstile(ts["data-sitekey"], url)
return self.session.post(url, data={"cf-turnstile-response": token})
raise Exception("Unknown CAPTCHA type")
def _solve_recaptcha(self, site_key, page_url):
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY, "method": "userrecaptcha",
"googlekey": site_key, "pageurl": page_url
})
return self._poll(resp.text.split("|")[1])
def _solve_turnstile(self, site_key, page_url):
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY, "method": "turnstile",
"sitekey": site_key, "pageurl": page_url
})
return self._poll(resp.text.split("|")[1])
def _poll(self, task_id):
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
})
if result.text == "CAPCHA_NOT_READY": continue
if result.text.startswith("OK|"): return result.text.split("|")[1]
raise Exception(result.text)
raise TimeoutError()
# Usage
scraper = ProtectedScraper()
html = scraper.scrape("https://example.com/data")
Dois detalhes fazem esse código aguentar produção. O token volta para o campo certo de cada tipo — g-recaptcha-response no reCAPTCHA, cf-turnstile-response no Turnstile — e trocar um pelo outro é a causa mais frequente do "a API respondeu OK, mas o site recusou". E o _poll consulta a cada 5 s porque, antes disso, a resposta é quase sempre CAPCHA_NOT_READY.
Padrão 2: resolver antes de carregar a página
Quando uma rota sempre exibe o mesmo desafio — uma busca autenticada, um formulário interno —, a detecção vira desperdício. Com a sitekey conhecida, resolva primeiro e envie o POST direto, economizando um round-trip por página.
def scrape_known_captcha_page(url, site_key):
# Solve before even loading the page
token = solve_recaptcha(site_key, url)
# Submit directly with token
resp = requests.post(url, data={
"g-recaptcha-response": token,
"query": "search term"
})
return resp.text
O cuidado aqui é a validade do token: resolva imediatamente antes do envio, nunca em lote no início da execução. Um token gerado no minuto zero de um job de 40 minutos já expirou quando chega a vez dele.
Padrão 3: ambientes atrás do Cloudflare
O interstício do Cloudflare funciona por outro caminho: em vez de um token para o formulário, a resolução produz um cookie de validação que a sessão passa a carregar. Aqui o ambiente de QA devolve cookie_qa_validacao:
def get_cloudflare_clearance(url, proxy):
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": "turnstile_staging",
"pageurl": url,
"proxy": proxy,
"proxytype": "HTTP"
})
task_id = resp.text.split("|")[1]
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
})
if result.text == "CAPCHA_NOT_READY": continue
if "cookie_qa_validacao" in result.text:
# Parse cookie_qa_validacao and user_agent from response
return result.text
raise TimeoutError()
Recebido o cookie, a regra é consistência: o mesmo egress autorizado e o mesmo user agent da resolução valem para as requisições seguintes. Mudou um dos dois, a validação cai e você volta ao interstício — sem erro útil.
Percorrendo várias páginas sem derrubar o job
Com os três padrões no lugar, falta o loop. A ideia é isolar a falha de cada página: se a página 7 quebrar, a 8 segue.
def scrape_multiple_pages(base_url, site_key, pages):
scraper = ProtectedScraper()
results = []
for page in pages:
url = f"{base_url}?page={page}"
try:
html = scraper.scrape(url)
soup = BeautifulSoup(html, "html.parser")
items = soup.find_all("div", class_="item")
results.extend([item.text.strip() for item in items])
print(f"Page {page}: {len(items)} items")
except Exception as e:
print(f"Page {page} failed: {e}")
time.sleep(random.uniform(2, 5))
return results
O intervalo aleatório (random.uniform(2, 5)) quebra o padrão metronômico que dispara verificação extra. O try/except impede que uma exceção encerre o job inteiro. E o log vira métrica de graça: se a contagem de itens zerar em várias páginas seguidas, o problema já não é CAPTCHA, é seletor de HTML desatualizado.
Troubleshooting: o que realmente quebra
| Sintoma | O que verificar |
|---|---|
| CAPTCHA em toda página | Reduza o volume, aumente o intervalo, faça rotação de proxy no egress autorizado |
| Token aceito pela API, recusado pelo site | Expirou (use em 120 s) ou campo errado: g-recaptcha-response vs cf-turnstile-response |
| Cloudflare barra com o cookie válido | User agent ou IP de saída mudou; mantenha os dois fixos |
| Responde 200, mas sem os dados | Redirecionamento ou cookie faltando após a validação |
CAPCHA_NOT_READY até o timeout |
Normal nas primeiras consultas; se persistir, confira sitekey e pageurl |
Perguntas frequentes
Dá para reaproveitar a sessão e resolver menos vezes?
Sim, na maioria dos casos. Se a sessão mantiver os cookies da primeira validação, as páginas seguintes do mesmo domínio costumam passar direto.
Como levar esse fluxo para o pipeline de testes da equipe?
Em staging com CAPTCHA ativo, resolver o desafio pela API permite exercitar o formulário de ponta a ponta sem desligar a proteção para rodar o teste. Guarde os IDs de tarefa nos logs de CI.
Por quanto tempo o token continua válido?
Trabalhe com cerca de 120 s, contados de quando a API devolve o token — não de quando você o usa. Tokens guardados para depois são a causa clássica de recusa silenciosa.
O que fazer quando o widget só aparece depois do JavaScript?
Renderize com Selenium, Puppeteer ou Playwright, extraia a sitekey e a URL do DOM já montado e só então chame a API. O guia como tratar CAPTCHA no Selenium com Python detalha esse caminho.
O coletor encontrou hCaptcha. E agora?
Não é suportado pela CaptchaAI, assim como o FunCaptcha (Arkose Labs). Os tipos cobertos são reCAPTCHA v2 e v3 (incluindo Enterprise), Cloudflare Turnstile e Challenge, GeeTest v3, imagem/OCR, grade de imagens e BLS, além de CaptchaFox, Friendly Captcha e Lemin em beta.