O token voltou resolvido e o formulário recusou assim mesmo. Antes de suspeitar da resolução, olhe o nome do campo: o script mandou g-recaptcha-response para uma página que hoje carrega Turnstile e espera cf-turnstile-response. reCAPTCHA v2 no login e Turnstile no checkout é rotina — a correção não é adivinhar o provedor, é ler o HTML a cada requisição e decidir ali qual method usar e em que campo devolver o token.
O padrão abaixo atende os dois provedores com a mesma chave de API da CaptchaAI: como reconhecer cada widget, qual parâmetro enviar e onde o token precisa chegar.
Resposta rápida: nunca fixe o provedor no código
- Detecte antes de resolver, pelo HTML que você acabou de receber — não pelo que a página tinha ontem.
- Troque apenas o
method:userrecaptchacomgooglekey, outurnstilecomsitekey. Os endpointsin.phperes.phpsão os mesmos. - Devolva o token no campo certo:
g-recaptcha-responseoucf-turnstile-response. - Redetecte a cada página; o provedor em cache é o que transforma um teste A/B do site em falha intermitente.
Como reconhecer reCAPTCHA v2 e Turnstile no HTML
A detecção confiável começa pelo nome da classe do container, não pelo data-sitekey — esse atributo é idêntico nos dois provedores. A URL do script serve como segunda confirmação.
| Provedor | Marcador no HTML | URL do script | Campo de resposta |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
O hCaptcha está na tabela porque você precisa distingui-lo do Turnstile. A CaptchaAI não resolve hCaptcha nem FunCaptcha (Arkose Labs), e o GeeTest v4 está anunciado como "em breve": se um desses widgets aparecer, o detector deve falhar de forma explícita.
Por que um mesmo site mistura dois provedores
Nenhum dos cenários abaixo é anomalia — todos aparecem em produção.
| Cenário | Como aparece |
|---|---|
| Páginas diferentes, provedores diferentes | Login com reCAPTCHA, checkout com Turnstile |
| Teste A/B entre provedores | A mesma URL mostra ora um widget, ora o outro |
| Migração em andamento | Telas antigas com reCAPTCHA, novas com Turnstile |
| Fallback de disponibilidade | O provedor principal não carrega e o secundário assume |
| Variação por região | reCAPTCHA para os EUA, Turnstile para a UE (RGPD) |
Cenário: dois widgets no mesmo fluxo de checkout
Uma equipe em São Paulo mantém testes de regressão do próprio checkout em staging.example.com. O login usa reCAPTCHA v2; a etapa de pagamento subiu depois com Turnstile. O mesmo teste atravessa dois provedores em menos de um minuto — e quebrava sempre que o script reaproveitava o widget visto no login.
Três ajustes resolveram o caso:
- Chamar a detecção em cada navegação, não uma única vez por sessão.
- Registrar no log o provedor detectado junto do tempo de resolução. Com os workers em
sa-east-1, o RTT é curto e quase todo o tempo do teste é tempo de resolução. - Usar dados fictícios nos formulários: logs de QA com dados pessoais reais entram no escopo da LGPD (RGPD em Portugal) como qualquer outra base.
Detecção automática e resolução em Python
O exemplo fecha o ciclo: baixa a página, identifica o provedor, envia a tarefa e devolve o token com o campo correto.
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://staging.example.com/qa-login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
Duas decisões merecem atenção. Cada provedor é testado com duas expressões, porque a ordem dos atributos varia conforme o template do site. E o CaptchaInfo carrega o response_field junto do method: quem escolhe o campo é a detecção, não o código que monta o envio.
O mesmo padrão em Node.js
Em JavaScript a lógica é idêntica: detectar, mapear para o method, enviar e consultar o resultado.
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://staging.example.com/qa-login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
Tempo de resolução e timeout do polling
Os dois provedores não levam o mesmo tempo. Pelas métricas publicadas da CaptchaAI, o Cloudflare Turnstile é resolvido em menos de 10 s e o reCAPTCHA v2 em menos de 60 s. O loop dos exemplos consulta o resultado a cada 5 segundos, em até 60 tentativas: folga suficiente para os dois. Trate CAPCHA_NOT_READY como estado normal e calibre o timeout pelo pior caso, o reCAPTCHA v2.
Custo: um plano cobre os dois provedores
A CaptchaAI cobra por thread simultânea, não por resolução, e cada plano inclui resoluções ilimitadas por thread, sem taxa por tipo de CAPTCHA. Tratar os dois provedores no mesmo pipeline não muda a conta: o que dimensiona o plano é a concorrência. O BASIC (US$ 15/mês, 5 threads) atende uma suíte de QA; o ADVANCE (US$ 90/mês, 50 threads) cobre dezenas de páginas em paralelo.
Erros comuns e como corrigir
| Sintoma | Causa | Correção |
|---|---|---|
| Provedor identificado errado | A expressão casou com outro elemento | Verifique a classe do provedor, não apenas o data-sitekey |
| Token recusado após resolução correta | Campo de resposta trocado | g-recaptcha-response para reCAPTCHA, cf-turnstile-response para Turnstile |
| O tipo muda entre visitas | Teste A/B ou seleção por região | Detecte em tempo de execução; não fixe o provedor |
| Dois widgets na mesma página | Um deles costuma estar oculto | Confira a visibilidade e resolva apenas o widget visível |
| Falha em página renderizada por script | Sem marcador no HTML inicial | Procure grecaptcha.render() ou turnstile.render() nos scripts |
Perguntas frequentes
A detecção por expressão regular funciona com widget renderizado por JavaScript?
Nem sempre. Se o HTML inicial não traz o container, o data-sitekey só existe depois que o script roda. Procure as chamadas grecaptcha.render() ou turnstile.render() nos scripts, ou obtenha o HTML já renderizado com um navegador headless.
Preciso de chaves ou planos separados para cada provedor?
Não. A mesma chave resolve os dois, e a cobrança por thread não distingue tipo de CAPTCHA. Dimensione o plano pelo número de resoluções simultâneas, não pela variedade de widgets.
E se o site passar a usar hCaptcha?
A CaptchaAI não suporta hCaptcha nem FunCaptcha (Arkose Labs). O detector deve reconhecer o marcador h-captcha, registrar o provedor e interromper o fluxo com uma mensagem clara — assim o teste falha em um ponto identificável, não em um envio sem explicação.
Posso reaproveitar o mesmo token em uma nova tentativa?
Não. Os tokens de reCAPTCHA v2 e de Turnstile são de uso único e de validade curta: valem para um envio, naquele formulário. Se a requisição falhar, resolva de novo em vez de reenviar o token anterior.
Artigos relacionados
- Como resolver o callback do reCAPTCHA v2 pela API
- Como diferenciar o desafio do Cloudflare do Turnstile
- GeeTest e Cloudflare Turnstile lado a lado
Próximas etapas
Um detector de poucas linhas evita dias de caça a falhas intermitentes. Pegue sua chave de API da CaptchaAI e coloque a detecção antes da resolução no próximo teste em staging.
Guias relacionados: