Tutorials

Como resolver o Cloudflare Turnstile com Node.js e a CaptchaAI

Seu script em Node.js recebeu uma página com a div cf-turnstile no lugar do formulário esperado? A resposta curta é que você não precisa abrir um navegador para seguir em frente: três requisições HTTP resolvem o caso. Você lê a sitekey no HTML, envia o desafio para a API da CaptchaAI e reenvia o formulário com o token no campo cf-turnstile-response.

O código usa fetch nativo e termina em uma classe reutilizável pronta para o seu worker. O Turnstile é plenamente suportado pela CaptchaAI e costuma ser resolvido em menos de 10 s.


O que você precisa antes de começar

  • Node.js 18 ou superior, porque o fetch já vem embutido no runtime.
  • Uma chave de API da CaptchaAI. O plano BASIC (US$ 15/mês, 5 threads) cobre o exemplo deste artigo; a cobrança é por thread simultânea, com resoluções ilimitadas no mês. Comece pelo guia de início rápido da CaptchaAI.
  • Um alvo autorizado. Os exemplos apontam para https://staging.example.com/qa-login, um endpoint fictício de homologação.

Guarde a chave em variável de ambiente: YOUR_API_KEY é só um marcador.


Como o fluxo funciona em três requisições

O modelo mental é o mesmo em qualquer linguagem:

  1. GET na página para achar a sitekey, que sempre começa com 0x.
  2. POST em in.php com method=turnstile, a sitekey e a pageurl; a API devolve um ID de tarefa.
  3. Consulta em res.php até o token voltar, seguida do POST no formulário.

A diferença em relação ao reCAPTCHA aparece no method (turnstile) e no campo de destino do token, cf-turnstile-response. Reaproveitar o campo de outro tipo de desafio é a causa número um de token recusado sem erro claro.


Passo 1: extrair a sitekey do Turnstile na página

A sitekey aparece em quatro lugares possíveis: no data-sitekey da div do widget, em outro elemento qualquer, em uma chamada turnstile.render ou solta em script inline. A função tenta os quatro padrões em ordem.

async function extractTurnstileSitekey(url) {
  const resp = await fetch(url, {
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    },
  });
  const html = await resp.text();

  // Method 1: data-sitekey attribute on Turnstile div
  const divMatch = html.match(
    /class=["'][^"]*cf-turnstile[^"]*["'][^>]*data-sitekey=["']([0-9x][A-Za-z0-9_-]+)["']/
  );
  if (divMatch) return divMatch[1];

  // Method 2: data-sitekey on any element (Turnstile keys start with 0x)
  const attrMatch = html.match(
    /data-sitekey=["'](0x[A-Za-z0-9_-]+)["']/
  );
  if (attrMatch) return attrMatch[1];

  // Method 3: In JavaScript turnstile.render call
  const jsMatch = html.match(
    /turnstile\.render\s*\([^,]+,\s*\{[^}]*sitekey\s*:\s*["']([0-9x][A-Za-z0-9_-]+)["']/
  );
  if (jsMatch) return jsMatch[1];

  // Method 4: Generic sitekey in inline script
  const inlineMatch = html.match(
    /sitekey\s*:\s*["'](0x[A-Za-z0-9_-]+)["']/
  );
  if (inlineMatch) return inlineMatch[1];

  return null;
}

Se ela retornar null em uma página que exibe o widget, o Turnstile é injetado por JavaScript: capture o HTML já renderizado com Puppeteer ou Playwright e aplique a mesma regex.


Passo 2: enviar o desafio à CaptchaAI e consultar o resultado

O envio é um POST com json: "1"; depois, um laço de polling consulta o resultado a cada 5 s, até 30 vezes.

const API_KEY = "YOUR_API_KEY";

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function solveTurnstile(sitekey, pageurl, action = null) {
  // Submit task
  const submitData = {
    key: API_KEY,
    method: "turnstile",
    sitekey: sitekey,
    pageurl: pageurl,
    json: "1",
  };

  if (action) {
    submitData.action = action;
  }

  const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
    method: "POST",
    body: new URLSearchParams(submitData),
  });
  const submitResult = await submitResp.json();

  if (submitResult.status !== 1) {
    throw new Error(`Submit error: ${submitResult.request}`);
  }

  const taskId = submitResult.request;
  console.log(`Task ID: ${taskId}`);

  // Poll for result
  for (let i = 0; i < 30; i++) {
    await sleep(5000);

    const pollResp = await fetch(
      `https://ocr.captchaai.com/res.php?${new URLSearchParams({
        key: API_KEY,
        action: "get",
        id: taskId,
        json: "1",
      })}`
    );
    const pollResult = await pollResp.json();

    if (pollResult.status === 1) {
      return pollResult.request;
    }

    if (pollResult.request === "ERROR_CAPTCHA_UNSOLVABLE") {
      throw new Error("Turnstile unsolvable");
    }
  }

  throw new Error("Solve timed out");
}

Três detalhes evitam dor de cabeça: espere alguns segundos antes da primeira consulta; trate ERROR_CAPTCHA_UNSOLVABLE como falha da tarefa e reenvie; e registre o taskId em log para rastrear lentidão depois.


Passo 3: devolver o token no campo cf-turnstile-response

O token só vale no mesmo formulário e na mesma URL de onde a sitekey saiu. Envie os campos originais mais o token, com os cabeçalhos esperados.

async function submitTurnstileForm(url, formData, token) {
  const body = new URLSearchParams({
    ...formData,
    "cf-turnstile-response": token,
  });

  const resp = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    },
    body,
  });

  return {
    status: resp.status,
    body: await resp.text(),
  };
}

O token tem vida curta e uso único: trate os passos 2 e 3 como uma sequência contínua, sem reaproveitar tokens entre execuções.


Fluxo de login completo em um ambiente de homologação

Juntando as três funções, um login automatizado de teste fica assim:

async function loginWithTurnstile(loginUrl, credentials) {
  // Step 1: Extract sitekey
  const sitekey = await extractTurnstileSitekey(loginUrl);
  if (!sitekey) {
    throw new Error("Turnstile sitekey not found");
  }
  console.log(`Sitekey: ${sitekey}`);

  // Step 2: Solve Turnstile
  const token = await solveTurnstile(sitekey, loginUrl);
  console.log(`Token: ${token.substring(0, 50)}...`);

  // Step 3: Submit form
  const result = await submitTurnstileForm(loginUrl, credentials, token);
  console.log(`Result: ${result.status}`);

  return result;
}

// Usage
const result = await loginWithTurnstile("https://staging.example.com/qa-login", {
  email: "[email protected]",
  password: "pass123",
});

É o formato típico de uma suíte de integração: credenciais fictícias, endpoint de staging e verificação do status.


Encapsulando tudo em uma classe reutilizável

Em produção, três funções soltas incomodam. A classe abaixo concentra detecção, envio e polling, guarda a chave em campo privado e expõe dois métodos públicos.

class TurnstileSolver {
  #apiKey;

  constructor(apiKey) {
    this.#apiKey = apiKey;
  }

  async solve(sitekey, pageurl, options = {}) {
    const taskId = await this.#submit(sitekey, pageurl, options);
    return await this.#poll(taskId);
  }

  async detectAndSolve(url) {
    const sitekey = await this.#detect(url);
    if (!sitekey) throw new Error("No Turnstile found");
    return await this.solve(sitekey, url);
  }

  async #detect(url) {
    const resp = await fetch(url, {
      headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" },
    });
    const html = await resp.text();
    const match = html.match(/data-sitekey=["'](0x[A-Za-z0-9_-]+)["']/);
    return match ? match[1] : null;
  }

  async #submit(sitekey, pageurl, options) {
    const body = new URLSearchParams({
      key: this.#apiKey,
      method: "turnstile",
      sitekey,
      pageurl,
      json: "1",
      ...(options.action && { action: options.action }),
      ...(options.cdata && { data: options.cdata }),
    });

    const resp = await fetch("https://ocr.captchaai.com/in.php", {
      method: "POST",
      body,
    });
    const data = await resp.json();

    if (data.status !== 1) throw new Error(`Submit: ${data.request}`);
    return data.request;
  }

  async #poll(taskId) {
    const params = new URLSearchParams({
      key: this.#apiKey,
      action: "get",
      id: taskId,
      json: "1",
    });

    for (let i = 0; i < 30; i++) {
      await new Promise((r) => setTimeout(r, 5000));
      const resp = await fetch(`https://ocr.captchaai.com/res.php?${params}`);
      const data = await resp.json();

      if (data.status === 1) return data.request;
      if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") {
        throw new Error("Unsolvable");
      }
    }
    throw new Error("Timed out");
  }
}

// Usage
const solver = new TurnstileSolver("YOUR_API_KEY");
const token = await solver.detectAndSolve("https://staging.example.com/qa-login");

O worker instancia a classe uma vez e chama detectAndSolve por item da fila. Como o faturamento é por thread simultânea, a concorrência deve acompanhar o plano: com 5 threads, cinco resoluções em voo.


Parâmetros action e cData: quando o Turnstile exige contexto extra

Algumas implementações do Turnstile incluem os parâmetros action e cData:

// Extract action from the page
function extractTurnstileAction(html) {
  const match = html.match(
    /data-action=["']([^"']+)["']|action\s*:\s*["']([^"']+)["']/
  );
  return match ? match[1] || match[2] : null;
}

// Solve with action
const token = await solver.solve(sitekey, pageurl, {
  action: "login",
  cdata: "session_abc123",
});

Quando o site define esses valores, enviá-los é obrigatório: sem a action correta, o token volta da API e é recusado pelo site — o famoso 403 depois de tudo parecer certo.


Verificação do token no seu próprio servidor

Do outro lado, um backend que valida tokens recebidos do próprio frontend usa o endpoint oficial da Cloudflare com a chave secreta:

async function verifyTurnstileToken(token, ip) {
  const resp = await fetch(
    "https://challenges.cloudflare.com/turnstile/v0/siteverify",
    {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        secret: "YOUR_TURNSTILE_SECRET_KEY",
        response: token,
        remoteip: ip,
      }),
    }
  );

  const data = await resp.json();
  return data.success;
}

Reproduzir essa verificação em ambiente controlado é a forma mais rápida de saber se o problema está no token ou na requisição.


Um cenário prático: monitoramento autorizado com workers em São Paulo

Pense em uma equipe brasileira que verifica diariamente um portal interno protegido por Turnstile, com workers em sa-east-1. Abrir um navegador completo a cada checagem custa memória e tempo; no fluxo em três requisições, cada verificação vira uma chamada HTTP leve que cabe em uma função serverless.

Dois pontos locais: meça o RTT até a API antes de fixar timeouts; e, se houver dado pessoal no caminho, considere as obrigações da LGPD (ou do RGPD, em Portugal) sobre o que vai para o log — guardar apenas taskId e status costuma bastar.


Erros comuns e como corrigir

Sintoma Causa provável Correção
Sitekey começa com 6Le É reCAPTCHA, não Turnstile Use method=userrecaptcha
Token recusado pelo site Sitekey errada ou token expirado Extraia a sitekey de novo e envie o formulário na sequência
Nenhuma sitekey encontrada O widget é carregado via JavaScript Renderize com Puppeteer ou Playwright e aplique a regex
ERROR_BAD_PARAMETERS Falta a sitekey ou a pageurl Confira se os dois campos vão no POST
Resposta 403 depois do envio Cabeçalhos inconsistentes com a sessão Repita os mesmos cabeçalhos do GET inicial

Se o token funciona no cURL e falha no script, o problema costuma estar na camada do navegador, não na resolução.


Perguntas frequentes

Preciso de Puppeteer ou Playwright para resolver o Turnstile?

Na maioria dos casos, não. Se a sitekey estiver no HTML do GET, o fluxo com fetch basta e custa muito menos recursos. O navegador só entra quando o widget é injetado dinamicamente ou a sessão depende de JavaScript.

Por quanto tempo o token do Turnstile continua válido?

Pouco tempo, e o uso é único. Envie o formulário na mesma execução em que o token chegou; guardá-lo para depois não funciona.

Quanto tempo leva uma resolução de Turnstile?

O Turnstile é resolvido em menos de 10 s no padrão da plataforma, com alta taxa de sucesso. O polling do exemplo consulta a cada 5 s até 30 vezes — margem folgada para picos.

Quantas resoluções consigo rodar em paralelo?

Tantas quantas forem as threads do plano, já que a cobrança é por thread simultânea. No BASIC (US$ 15/mês, 5 threads) são cinco tarefas em voo. Se a fila crescer, suba de plano em vez de abrir mais conexões.


Resumo

Três movimentos resumem o caminho em Node.js: achar a sitekey que começa com 0x, resolver o desafio pela API da CaptchaAI com method=turnstile e devolver o resultado em cf-turnstile-response. O resto é ajustar timeouts e concorrência ao seu plano.

Artigos relacionados

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