Playwright for Node.js offers the best combination of speed, stealth-configuredion, and multi-browser support. This guide covers the complete integration with CaptchaAI for all CAPTCHA types.
Prerequisites
npm install playwright
npx playwright install chromium
Stealth-configuredion browser setup
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 };
}
CaptchaAI solver
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");
}
reCAPTCHA v2 with Playwright
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 with Playwright
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;
}
Auto-detect and solve
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;
}
}
Image CAPTCHA with Playwright
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;
}
Route interception for CAPTCHA parameters
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;
}
Complete automation class
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://example.com/login",
{
"#email": "user@example.com",
"#password": "pass123",
},
"#login-btn"
);
console.log(`Redirected to: ${result}`);
} finally {
await bot.stop();
}
Playwright vs Puppeteer comparison
| Feature | Playwright | Puppeteer |
|---|---|---|
| Multi-browser | Chromium, Firefox, WebKit | Chromium only |
| API style | Locator-based | Selector-based |
| Auto-waiting | Built-in | Manual waits |
| Network interception | Route-based | Request-based |
| Stealth-configuredion | Good defaults | Needs stealth plugin |
| TypeScript | Native | Community types |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
page.evaluate returns null |
Element not loaded | Use waitForSelector first |
| Turnstile not detected | Loaded via JS after page load | Wait for .cf-turnstile selector |
| Token injection doesn't submit | Missing callback trigger | Call reCAPTCHA callback explicitly |
| Browser detection | Missing init script | Add webdriver override |
networkidle timeout |
Long-polling scripts | Use domcontentloaded instead |
Frequently asked questions
Should I use Playwright or Puppeteer for new projects?
Playwright. It has better defaults, native TypeScript support, auto-waiting, and multi-browser testing.
Can I run Playwright in headless mode?
Yes. Set headless: true in launch(). CaptchaAI solves independently, so headless doesn't affect solve success.
How do I handle multiple CAPTCHAs on one page?
Call detectAndSolve() after each form step. Some pages have CAPTCHAs on multiple steps.
Summary
Node.js Playwright + CaptchaAI provides a modern automation stack with auto-detection, route interception, and multi-CAPTCHA support. The PlaywrightAutomation class handles the complete login-with-CAPTCHA workflow.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.