Para resolver reCAPTCHA v2, Cloudflare Turnstile ou CAPTCHA de imagem dentro de um script Playwright em Node.js, o caminho é sempre o mesmo: capturar a sitekey na página, enviar o desafio à API da CaptchaAI, aguardar o token e escrevê-lo de volta no formulário. Este guia monta esse fluxo do início e termina em uma classe PlaywrightAutomation reutilizável que resolve login com CAPTCHA de ponta a ponta.
O Playwright encaixa bem nesse tipo de automação porque roda em Chromium, Firefox e WebKit com a mesma API, tem espera automática e dispensa plugins extras nos cenários de QA. Se o seu público está no Brasil, rodar os workers em uma região próxima — por exemplo, AWS sa-east-1, em São Paulo — reduz o RTT até a página alvo.
Neste guia você vai:
- Subir um navegador com padrões estáveis para testes autorizados
- Enviar e consultar desafios pela API da CaptchaAI (
in.phperes.php) - Resolver reCAPTCHA v2, Turnstile e CAPTCHA de imagem no mesmo projeto
- Reunir tudo em uma classe de automação pronta para reutilizar
Instalação e pré-requisitos
Instale o Playwright e baixe o Chromium antes de começar:
npm install playwright
npx playwright install chromium
Configuração do navegador para QA
Suba o navegador com padrões estáveis para os seus testes autorizados. O script de inicialização ajusta a flag navigator.webdriver, um sinal que muitas páginas verificam:
const { chromium } = require("playwright");
async function createBrowser() {
const browser = await chromium.launch({
headless: false,
args: ["--disable-blink-features=AutomationControlled"],
});
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
locale: "en-US",
});
// Remove Playwright detection
await context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
delete navigator.__proto__.webdriver;
});
const page = await context.newPage();
return { browser, context, page };
}
Função de resolução com a CaptchaAI
Esta função concentra as duas chamadas da API: in.php envia a tarefa e res.php consulta o resultado. O laço faz o polling a cada 5 segundos, com um teto de 30 tentativas antes de estourar o timeout:
const API_KEY = "YOUR_API_KEY";
async function solveCaptcha(method, params) {
// Submit
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) throw new Error(`Submit: ${submitData.request}`);
const taskId = submitData.request;
// Poll
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: API_KEY,
action: "get",
id: taskId,
json: "1",
})}`
);
const data = await pollResp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
}
throw new Error("Timed out");
}
Trabalhe sempre contra o seu próprio ambiente de staging e, quando a automação envolver coleta de dados, considere as obrigações da LGPD. Este fluxo é para QA e integrações autorizadas, não para acessar sistemas de terceiros sem permissão.
reCAPTCHA v2 com Playwright
Para o reCAPTCHA v2, leia a data-sitekey da página, envie com o método userrecaptcha e escreva o token na textarea g-recaptcha-response. Muitos formulários só validam quando o callback do widget é disparado, então o trecho percorre a configuração do grecaptcha e chama a função manualmente:
async function solveRecaptchaV2(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector("[data-sitekey]");
return el ? el.getAttribute("data-sitekey") : null;
});
if (!sitekey) throw new Error("Sitekey not found");
// Solve
const token = await solveCaptcha("userrecaptcha", {
googlekey: sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
const textarea = document.getElementById("g-recaptcha-response");
if (textarea) {
textarea.value = t;
textarea.style.display = "block";
}
// Trigger callback
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
for (const prop in clients[key]) {
try {
const cb = clients[key][prop];
if (cb && typeof cb.callback === "function") cb.callback(t);
} catch {}
}
}
}
}, token);
return token;
}
Cloudflare Turnstile com Playwright
O Turnstile é mais direto: o método é turnstile e o token vai para o campo cf-turnstile-response. Como o widget nem sempre traz a classe .cf-turnstile, o trecho tem um plano B — procurar qualquer data-sitekey que comece com 0x:
async function solveTurnstile(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector(".cf-turnstile[data-sitekey]");
if (el) return el.getAttribute("data-sitekey");
// Fallback: any data-sitekey starting with 0x
const all = document.querySelectorAll("[data-sitekey]");
for (const item of all) {
const key = item.getAttribute("data-sitekey");
if (key && key.startsWith("0x")) return key;
}
return null;
});
if (!sitekey) throw new Error("Turnstile sitekey not found");
// Solve
const token = await solveCaptcha("turnstile", {
sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
return token;
}
Detecção e resolução automática
Em vez de escolher a função na mão, deixe o código identificar o tipo de desafio presente na página e despachar para o método certo. Assim o mesmo fluxo cobre reCAPTCHA, Turnstile e CAPTCHA de imagem:
async function detectAndSolve(page) {
const captchaInfo = await page.evaluate(() => {
// Check reCAPTCHA
const recaptcha = document.querySelector("[data-sitekey]");
if (
recaptcha &&
(document.querySelector(".g-recaptcha") ||
document.querySelector('script[src*="recaptcha"]'))
) {
return { type: "recaptcha", sitekey: recaptcha.getAttribute("data-sitekey") };
}
// Check Turnstile
const turnstile = document.querySelector(".cf-turnstile[data-sitekey]");
if (turnstile) {
return { type: "turnstile", sitekey: turnstile.getAttribute("data-sitekey") };
}
// Check image CAPTCHA
const captchaImg = document.querySelector(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
);
if (captchaImg) {
return { type: "image" };
}
return { type: null };
});
if (!captchaInfo.type) return null;
console.log(`Detected: ${captchaInfo.type}`);
switch (captchaInfo.type) {
case "recaptcha":
return await solveCaptcha("userrecaptcha", {
googlekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "turnstile":
return await solveCaptcha("turnstile", {
sitekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "image":
return await solveImageCaptcha(page);
default:
return null;
}
}
CAPTCHA de imagem com Playwright
Para desafios de imagem (OCR), tire um screenshot do elemento, converta para base64 e envie com o método base64. Depois é só digitar a resposta no campo de entrada:
async function solveImageCaptcha(page) {
const captchaImg = page.locator(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
).first();
// Screenshot the CAPTCHA element
const imgBuffer = await captchaImg.screenshot();
const imgBase64 = imgBuffer.toString("base64");
// Solve via CaptchaAI
const answer = await solveCaptcha("base64", { body: imgBase64 });
// Type the answer
const input = page.locator(
'input[name="captcha"], input[name="code"], input.captcha-input'
).first();
await input.fill(answer);
return answer;
}
Interceptação de rotas para capturar parâmetros do CAPTCHA
Alguns desafios, como o GeeTest, entregam parâmetros dinâmicos em respostas de rede. Escute os eventos response da página para capturar esses valores antes de montar a tarefa:
async function interceptCaptchaRoutes(page, url) {
const captchaParams = {};
// Intercept responses
page.on("response", async (response) => {
const respUrl = response.url();
// GeeTest parameters
if (respUrl.includes("geetest") || respUrl.includes("gt=")) {
try {
const data = await response.json();
if (data.gt) {
captchaParams.type = "geetest";
captchaParams.gt = data.gt;
captchaParams.challenge = data.challenge;
}
} catch {}
}
});
await page.goto(url, { waitUntil: "networkidle" });
return captchaParams;
}
Classe de automação completa
Agora reúna tudo em uma única classe. A PlaywrightAutomation cuida do ciclo inteiro — subir o navegador, preencher o formulário, detectar e resolver o CAPTCHA, inserir o token e enviar. O exemplo de uso aponta para um endpoint de staging, o padrão recomendado para testes autorizados:
const { chromium } = require("playwright");
class PlaywrightAutomation {
#apiKey;
#browser;
#context;
#page;
constructor(apiKey) {
this.#apiKey = apiKey;
}
async start(headless = false) {
this.#browser = await chromium.launch({
headless,
args: ["--disable-blink-features=AutomationControlled"],
});
this.#context = await this.#browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
});
await this.#context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
});
this.#page = await this.#context.newPage();
}
async stop() {
await this.#browser?.close();
}
async navigate(url) {
await this.#page.goto(url, { waitUntil: "networkidle" });
}
async fillForm(fields) {
for (const [selector, value] of Object.entries(fields)) {
await this.#page.fill(selector, value);
}
}
async solveCaptcha() {
return await detectAndSolve(this.#page);
}
async submit(selector = 'button[type="submit"]') {
await this.#page.click(selector);
await this.#page.waitForLoadState("networkidle");
return this.#page.url();
}
async loginWithCaptcha(url, fields, submitSelector) {
await this.navigate(url);
await this.fillForm(fields);
const token = await this.solveCaptcha();
if (token) {
// Inject token
await this.#page.evaluate((t) => {
const re = document.getElementById("g-recaptcha-response");
if (re) re.value = t;
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
}
return await this.submit(submitSelector);
}
get page() {
return this.#page;
}
}
// Usage
const bot = new PlaywrightAutomation("YOUR_API_KEY");
await bot.start();
try {
const result = await bot.loginWithCaptcha(
"https://staging.example.com/qa-login",
{
"#email": "user@example.com",
"#password": "pass123",
},
"#login-btn"
);
console.log(`Redirected to: ${result}`);
} finally {
await bot.stop();
}
Playwright vs Puppeteer: quando usar cada um
Para projetos novos que precisam de vários navegadores e espera automática, o Playwright costuma ser a opção mais confortável. O Puppeteer segue sólido para quem já vive no ecossistema Chrome:
| Recurso | Playwright | Puppeteer |
|---|---|---|
| Multi-navegador | Chromium, Firefox, WebKit | Somente Chromium |
| Estilo de API | Baseado em localizador | Baseado em seletor |
| Espera automática | Integrada | Esperas manuais |
| Interceptação de rede | Baseada em rota | Baseada em requisição |
| Configuração para QA | Bons padrões | Requer plugin adicional |
| TypeScript | Nativo | Tipos da comunidade |
Solução de problemas
Os sintomas mais comuns na integração e como resolvê-los:
| Sintoma | Causa | Correção |
|---|---|---|
page.evaluate retorna nulo |
Elemento ainda não carregou | Use waitForSelector antes |
| Cloudflare Turnstile não é detectado | Widget carrega via JS depois da página | Aguarde o seletor .cf-turnstile |
| Formulário não avança após inserir o token | Callback não foi disparado | Chame o callback do reCAPTCHA manualmente |
| Página identifica a automação | Script de inicialização ausente | Adicione a substituição de navigator.webdriver |
Timeout em networkidle |
Scripts de long-polling na página | Use domcontentloaded no lugar |
Perguntas frequentes
A CaptchaAI resolve Turnstile e reCAPTCHA com a mesma função?
Sim. A função solveCaptcha() é genérica: você só troca o method (turnstile, userrecaptcha, base64) e os parâmetros do desafio. A CaptchaAI resolve reCAPTCHA v2/v3, Cloudflare Turnstile e Challenge, GeeTest v3 e CAPTCHAs de imagem/OCR pelo mesmo endpoint.
Preciso configurar proxies para a resolução funcionar?
Não. A CaptchaAI resolve o desafio do lado do servidor e devolve o token; o Playwright continua acessando o site pela sua própria rede. Proxy é uma decisão de infraestrutura sua, não um requisito da API.
O hCaptcha é suportado neste fluxo?
Não. O hCaptcha e o FunCaptcha (Arkose Labs) não são suportados atualmente. Para os tipos suportados — reCAPTCHA, Turnstile, GeeTest v3 e imagem — o fluxo deste guia funciona sem alterações.
Quanto custa resolver CAPTCHAs em volume?
Os planos são cobrados por thread simultânea, com resolução ilimitada por thread no mês. O BASIC custa US$ 15/mês com 5 threads; se você processa muitos logins em paralelo, o STANDARD (US$ 30/mês, 15 threads) ou o ADVANCE (US$ 90/mês, 50 threads) ampliam a concorrência sem cobrança por CAPTCHA resolvido.
Conclusão
Com o Playwright no Node.js você monta uma pilha de automação moderna: detecção automática do tipo de desafio, interceptação de rotas e suporte a vários formatos de CAPTCHA. A classe PlaywrightAutomation concentra todo o fluxo de login com CAPTCHA em um único componente reutilizável — pronto para os seus testes de QA autorizados.