Troubleshooting

Lidando com reCAPTCHA v2 e Cloudflare Turnstile no mesmo site

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: userrecaptcha com googlekey, ou turnstile com sitekey. Os endpoints in.php e res.php são os mesmos.
  • Devolva o token no campo certo: g-recaptcha-response ou cf-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

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:

Os comentários estão desativados para este artigo.