Skrip Puppeteer Anda berjalan mulus sampai halaman target menyajikan reCAPTCHA atau Cloudflare Turnstile — lalu macet total di langkah submit. Solusinya bukan melatih model OCR sendiri atau menyewa tim solving manual: panggil API CaptchaAI dari dalam skrip Node.js yang sama. Puppeteer mengekstrak sitekey dari DOM, CaptchaAI menyelesaikan tantangannya di server, dan Puppeteer menyuntikkan token yang dikembalikan sebelum form dikirim.
Panduan ini membangun integrasi lengkapnya langkah demi langkah — modul solver untuk reCAPTCHA v2 dan Turnstile, konfigurasi browser, penanganan callback JavaScript untuk situs yang tidak memakai submit formulir biasa, sampai contoh kode produksi yang siap dijalankan. Di bagian akhir ada skenario nyata dan gambaran biaya untuk tim scraping kecil.
Persiapan: tools dan API key CaptchaAI
Siapkan empat hal berikut. Kunci API didapat gratis saat mendaftar di captchaai.com — dari kunci ini semua request ke in.php dan res.php diautentikasi.
| Persyaratan | Detail |
|---|---|
| Node.js 16+ | Dengan npm |
| Puppeteer | npm install puppeteer |
| axios | npm install axios |
| Kunci API CaptchaAI | Dari captchaai.com |
Alur kerja: kirim → simpan task ID → polling → pakai token
Integrasi Puppeteer dengan CaptchaAI selalu mengikuti pola empat langkah yang sama, apa pun tipe CAPTCHA-nya:
- Puppeteer membuka halaman yang menyajikan CAPTCHA dan menunggu widget-nya termuat
- Skrip mengekstrak sitekey dari elemen DOM (
.g-recaptcha,.cf-turnstile, dan sejenisnya) - CaptchaAI menerima sitekey dan
pageurl, lalu menyelesaikan tantangan di server-nya sendiri - Skrip menyuntikkan token yang dikembalikan ke halaman, lalu melanjutkan submit atau memicu callback
Langkah 1: Bangun modul solver reCAPTCHA v2 dan Turnstile
Pisahkan logika solve ke modul sendiri (solver.js) supaya bisa dipakai ulang di skrip mana pun. Modul ini punya dua fungsi terpisah — satu untuk reCAPTCHA v2 dengan method=userrecaptcha, satu lagi untuk Turnstile dengan method=turnstile — tapi keduanya memakai pola kirim-lalu-polling yang identik: submit ke in.php, simpan task_id yang dikembalikan, lalu polling res.php setiap POLL_INTERVAL sampai statusnya bukan lagi CAPCHA_NOT_READY.
// solver.js
const axios = require("axios");
const API_KEY = "YOUR_API_KEY";
const POLL_INTERVAL = 5000;
const MAX_ATTEMPTS = 60;
async function solveRecaptchaV2(siteKey, pageUrl) {
// Submit task
const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
},
});
if (!submitResp.data.startsWith("OK|")) {
throw new Error(`Submit failed: ${submitResp.data}`);
}
const taskId = submitResp.data.split("|")[1];
console.log(`Task submitted: ${taskId}`);
// Poll for result
for (let i = 0; i < MAX_ATTEMPTS; i++) {
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId },
});
if (result.data === "CAPCHA_NOT_READY") continue;
if (result.data.startsWith("OK|")) {
return result.data.split("|")[1];
}
throw new Error(`Solve failed: ${result.data}`);
}
throw new Error("Solve timed out");
}
async function solveTurnstile(siteKey, pageUrl) {
const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
params: {
key: API_KEY,
method: "turnstile",
sitekey: siteKey,
pageurl: pageUrl,
},
});
if (!submitResp.data.startsWith("OK|")) {
throw new Error(`Submit failed: ${submitResp.data}`);
}
const taskId = submitResp.data.split("|")[1];
for (let i = 0; i < MAX_ATTEMPTS; i++) {
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId },
});
if (result.data === "CAPCHA_NOT_READY") continue;
if (result.data.startsWith("OK|")) return result.data.split("|")[1];
throw new Error(`Solve failed: ${result.data}`);
}
throw new Error("Solve timed out");
}
module.exports = { solveRecaptchaV2, solveTurnstile };
MAX_ATTEMPTS = 60 dan POLL_INTERVAL = 5000 memberi batas tunggu sekitar 5 menit sebelum skrip menyerah — longgar untuk CAPTCHA yang antre di jam sibuk, tapi tetap mencegah proses menggantung kalau task-nya gagal.
Langkah 2: Konfigurasi browser Puppeteer
Konfigurasi Puppeteer di langkah ini memakai mode standar. Flag --no-sandbox dan --disable-setuid-sandbox umum diperlukan saat Puppeteer jalan di dalam container atau CI, dan user agent diset eksplisit supaya konsisten di setiap run.
const puppeteer = require("puppeteer");
async function createBrowser() {
const browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
],
});
const page = await browser.newPage();
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
);
// Hide automation indicators
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "webdriver", { get: () => false });
});
return { browser, page };
}
page.evaluateOnNewDocument menyelaraskan satu properti otomasi bawaan browser supaya konsisten dengan instalasi Chrome biasa — sekadar penyelarasan standar, karena tugas berat menyelesaikan CAPTCHA ada di sisi CaptchaAI, bukan di browser.
Langkah 3: Ekstrak sitekey dan suntikkan token ke halaman
Fungsi berikut menyatukan Langkah 1 dan 2: navigasi ke halaman, ambil sitekey lewat page.$eval, panggil solveRecaptchaV2, lalu suntikkan token ke elemen tersembunyi g-recaptcha-response sebelum tombol submit diklik.
const { solveRecaptchaV2 } = require("./solver");
async function scrapeWithCaptcha(url) {
const { browser, page } = await createBrowser();
try {
await page.goto(url, { waitUntil: "networkidle2" });
// Extract site key
const siteKey = await page.$eval(
".g-recaptcha",
(el) => el.getAttribute("data-sitekey")
);
console.log("Site key:", siteKey);
// Solve with CaptchaAI
const token = await solveRecaptchaV2(siteKey, url);
console.log("Token received:", token.substring(0, 50));
// Inject token
await page.evaluate((token) => {
document.getElementById("g-recaptcha-response").innerHTML = token;
document.getElementById("g-recaptcha-response").style.display = "";
}, token);
// Submit the form
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: "networkidle2" });
// Scrape the content
const content = await page.content();
console.log("Page loaded successfully");
return content;
} finally {
await browser.close();
}
}
Perhatikan urutannya: token disuntikkan lebih dulu, baru submit diklik. Token reCAPTCHA berumur pendek, jadi tekan jeda antara solveRecaptchaV2 selesai dan klik submit seminimal mungkin.
Langkah 4: Tangani callback JavaScript reCAPTCHA
Sebagian situs — terutama yang dibangun dengan framework SPA — tidak memakai submit formulir biasa. reCAPTCHA di situs itu dipasang lewat callback JavaScript yang harus dipanggil manual begitu token diterima; klik tombol submit saja tidak memicu apa-apa karena tombolnya menunggu callback tereksekusi lebih dulu.
// Trigger the reCAPTCHA callback
await page.evaluate((token) => {
// Method 1: Direct callback
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach((key) => {
const client = clients[key];
// Find the callback function
const findCallback = (obj) => {
for (const prop in obj) {
if (typeof obj[prop] === "function") {
obj[prop](token);
return true;
}
if (typeof obj[prop] === "object" && obj[prop] !== null) {
if (findCallback(obj[prop])) return true;
}
}
return false;
};
findCallback(client);
});
}
}, token);
Kode ini menelusuri objek ___grecaptcha_cfg global milik reCAPTCHA, menemukan fungsi callback yang terdaftar untuk widget aktif, lalu memanggilnya langsung dengan token sebagai argumen. Kalau situs tidak mendefinisikan callback publik, submit formulir di Langkah 3 tetap jadi pilihan pertama — pola ini baru diperlukan saat submit biasa tidak memicu navigasi.
Contoh lengkap: dari navigasi sampai submit
Berikut versi ringkas yang menyatukan solver dan Puppeteer dalam satu file, siap dijalankan dengan node index.js setelah API_KEY diisi.
const puppeteer = require("puppeteer");
const axios = require("axios");
const API_KEY = "YOUR_API_KEY";
async function solveCaptcha(siteKey, pageUrl) {
const submit = await axios.get("https://ocr.captchaai.com/in.php", {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
},
});
const taskId = submit.data.split("|")[1];
while (true) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId },
});
if (result.data === "CAPCHA_NOT_READY") continue;
if (result.data.startsWith("OK|")) return result.data.split("|")[1];
throw new Error(result.data);
}
}
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: [],
});
const page = await browser.newPage();
try {
await page.goto("https://staging.example.com/qa-login", {
waitUntil: "networkidle2",
});
// Get the site key
const siteKey = await page.$eval(".g-recaptcha", (el) =>
el.getAttribute("data-sitekey")
);
// Solve
const token = await solveCaptcha(siteKey, page.url());
// Inject and submit
await page.evaluate((t) => {
document.getElementById("g-recaptcha-response").innerHTML = t;
}, token);
await page.click("#submit-btn");
await page.waitForNavigation();
console.log("Done:", page.url());
} finally {
await browser.close();
}
})();
URL pada page.goto di atas memakai domain staging sebagai contoh — ganti dengan target Anda sendiri sebelum dijalankan di production, dan pastikan Anda memang berwenang menguji halaman tersebut.
Skenario nyata: monitoring harga e-commerce dengan tim scraping kecil
Pola ini paling sering dipakai tim price-monitoring dan agensi data kecil di Indonesia — banyak beroperasi lewat kontrak freelance ala Upwork atau Fastwork — yang menjalankan puluhan job Puppeteer paralel memantau harga di situs berpelindung Cloudflare Turnstile atau reCAPTCHA. CaptchaAI menagih per thread aktif, bukan per solve: paket BASIC ($15/bulan, 5 thread) biasanya cukup untuk uji coba awal, dan tim naik ke ADVANCE ($90/bulan, 50 thread) begitu volume job harian meningkat, tanpa biaya tambahan per CAPTCHA di kedua paket itu.
Server job di region AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) umumnya tetap berlatensi rendah ke endpoint CaptchaAI; sisakan jeda polling yang longgar untuk koneksi mobile-first yang kadang kurang stabil. Untuk alur yang menyentuh data pihak lain, pastikan target adalah sistem yang memang berwenang Anda uji, sejalan dengan UU Pelindungan Data Pribadi (UU 27/2022) dan UU ITE.
Mengatasi masalah umum integrasi
Sebagian besar kegagalan bukan soal API CaptchaAI, tapi timing atau selector di sisi Puppeteer:
| Masalah | Penyebab | Solusi |
|---|---|---|
page.$eval gagal |
CAPTCHA dimuat setelah render awal | Gunakan page.waitForSelector('.g-recaptcha') sebelum ekstraksi |
| Token tidak berfungsi | Kedaluwarsa sebelum diserahkan | Suntikkan token segera setelah solveRecaptchaV2 selesai |
| Situs mendeteksi Puppeteer | Konfigurasi browser masih default dan mencolok | Pasang puppeteer-extra dengan plugin deteksi-otomatisasi (mode standar) |
Navigation timeout |
Halaman tidak bernavigasi setelah token dikirim | Periksa apakah situs memakai AJAX, bukan submit formulir biasa |
| Polling lambat di jaringan tidak stabil | Interval terlalu rapat untuk koneksi mobile-first | Naikkan POLL_INTERVAL, pertahankan MAX_ATTEMPTS longgar |
Pertanyaan umum
Apakah kombinasi Puppeteer dan CaptchaAI menambah latensi berarti ke pipeline scraping?
Tidak signifikan untuk sebagian besar kasus. Waktu tambahan yang muncul adalah waktu solve CaptchaAI sendiri, ditambah jeda polling yang Anda atur di POLL_INTERVAL — bukan overhead dari sisi Puppeteer. Job paralel tetap berjalan normal selama alokasi thread mencukupi.
Apakah menjalankan banyak instance Puppeteer paralel berarti saya butuh thread CaptchaAI sebanyak itu juga?
Kurang lebih ya — setiap instance Puppeteer yang sedang menunggu solve memakai satu thread CaptchaAI yang aktif. Kalau job paralel lebih banyak dari alokasi thread di paket Anda, request tambahan akan antre sampai thread yang sedang dipakai selesai; naikkan paket (misalnya dari BASIC ke ADVANCE) kalau antrean mulai jadi bottleneck.
Bisakah CaptchaAI menyelesaikan Cloudflare Turnstile di script Puppeteer yang sama dengan reCAPTCHA v2?
Ya. Kedua fungsi solver — solveRecaptchaV2 dan solveTurnstile — hidup di modul yang sama dan dipanggil sesuai tipe CAPTCHA yang terdeteksi di halaman; cukup ekstrak sitekey dari elemen .cf-turnstile dan kirim dengan method=turnstile.
Apa yang terjadi kalau token reCAPTCHA kedaluwarsa sebelum sempat disuntikkan ke form?
Situs akan menolak submit dan biasanya meminta CAPTCHA baru. Token reCAPTCHA hanya valid dalam rentang waktu singkat setelah diterbitkan, jadi tekan jeda antara respons CaptchaAI dan aksi submit seminimal mungkin — jangan menyisipkan proses lain di antara keduanya.
Bagaimana menangani beberapa CAPTCHA dengan tipe berbeda dalam satu alur scraping?
Deteksi tipe CAPTCHA di tiap halaman terlebih dulu (elemen .g-recaptcha vs .cf-turnstile), lalu panggil fungsi solver yang sesuai. Untuk beberapa CAPTCHA di halaman yang sama, ekstrak tiap sitekey secara terpisah dan jalankan solve-nya paralel dengan Promise.all().
Panduan terkait
- Cara menyelesaikan CAPTCHA dengan Selenium dan Python
- Solve CAPTCHA di Playwright: panduan integrasi
- Scraping Node.js yang tahan CAPTCHA: panduan lengkap