Token Turnstile tidak pernah lahir dari kode Node.js Anda — token itu hanya dihasilkan oleh widget Cloudflare di sisi browser. Karena itu alurnya selalu tiga gerakan yang sama: ambil sitekey dari HTML halaman, kirim ke CaptchaAI untuk diselesaikan, lalu tempelkan token yang kembali ke field cf-turnstile-response saat form dikirim. Semua contoh di bawah memakai fetch bawaan Node.js 18+, tanpa browser headless.
Yang perlu disiapkan
- Node.js 18+ —
fetchsudah native, jadiaxiosmaupunnode-fetchtidak diperlukan. - API key CaptchaAI dari dashboard akun Anda.
- URL halaman yang benar-benar memuat widget Turnstile, bukan halaman hasil redirect.
Turnstile didukung penuh lewat parameter method=turnstile. Catatan: hCaptcha dan FunCaptcha (Arkose Labs) belum didukung, dan GeeTest v4 masih segera hadir.
Peta alur sebelum menulis kode
Empat langkah ini berlaku untuk semua tipe CAPTCHA di CaptchaAI, termasuk Turnstile:
- Kirim task ke
in.phpberisisitekeydanpageurl. - Simpan task ID yang dikembalikan pada field
request. - Polling
res.phpsampai jawabannya bukan lagiCAPCHA_NOT_READY. - Pakai token pada request berikutnya sebelum masa berlakunya habis.
Turnstile termasuk tipe cepat: penyelesaiannya di bawah 10 detik dengan tingkat keberhasilan tinggi pada tipe yang didukung. Jeda polling 5 detik sudah pas — lebih rapat hanya menambah request.
Langkah 1: ambil sitekey Turnstile dari HTML
Sitekey Turnstile selalu diawali 0x — itu pembeda paling cepat dari reCAPTCHA yang diawali 6L. Tidak semua situs menaruhnya di tempat yang sama: sebagian memakai atribut data-sitekey pada div.cf-turnstile, sebagian lain menyuntikkannya lewat turnstile.render() di script inline. Fungsi berikut mencoba empat pola berurutan, dari yang paling spesifik ke yang paling longgar.
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;
}
Langkah 2: kirim task ke CaptchaAI lalu ambil tokennya
Dua hal yang wajib benar di sini: method harus turnstile, dan pageurl harus persis sama dengan URL halaman tempat widget dirender. Menyertakan json: "1" membuat respons datang sebagai JSON sehingga Anda cukup memeriksa status === 1.
Setelah task diterima, field request berisi task ID. Loop polling di bawah berjalan maksimal 30 kali dengan jeda 5 detik, jadi batas waktu efektifnya sekitar 150 detik. Kode ERROR_CAPTCHA_UNSOLVABLE dihentikan lebih awal karena mengulanginya tidak mengubah hasil; yang benar adalah memeriksa ulang sitekey dan pageurl.
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");
}
Langkah 3: kirim token sebagai cf-turnstile-response
Nama field-nya tidak bisa ditawar: cf-turnstile-response. Memakai nama field milik widget lain adalah penyebab paling umum form ditolak tanpa pesan error yang jelas. Token Turnstile berumur pendek dan sekali pakai, jadi jangan menyelesaikan CAPTCHA jauh sebelum form siap dikirim.
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(),
};
}
Menyatukan ketiganya dalam satu alur login
Digabung, ketiga fungsi tadi menjadi satu alur yang bisa dipanggil dari worker mana pun. Setiap langkah gagal dengan pesan spesifik, jadi log produksi langsung menunjuk tahap yang bermasalah.
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: "user@example.com",
password: "pass123",
});
Kelas solver yang layak masuk produksi
Untuk pemakaian berulang, bungkus semuanya dalam satu kelas. API key disimpan di private field #apiKey agar tidak bocor ke log, deteksi sitekey dan pemanggilan API dipisah jadi method privat, dan detectAndSolve() menyediakan jalur satu baris untuk kasus paling umum. Ambil API key dari environment variable; placeholder YOUR_API_KEY di bawah hanya untuk keterbacaan contoh.
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");
Menangani parameter action dan cData
Sebagian implementasi Turnstile menambahkan parameter action dan cData. Keduanya ikut diperhitungkan saat Cloudflare memvalidasi token: kalau halaman memakainya sementara task Anda tidak, token lolos dari CaptchaAI tetapi ditolak server tujuan. Periksa data-action di HTML atau action: di script inline sebelum menyimpulkan parameter ini tidak dipakai.
// 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",
});
Memverifikasi token di sisi server Anda
Bagian ini relevan kalau Anda juga memasang Turnstile di aplikasi sendiri, misalnya di staging untuk menguji alur otomatisasi. Endpoint siteverify Cloudflare mengembalikan success: true untuk token valid; simpan YOUR_TURNSTILE_SECRET_KEY hanya di environment server.
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;
}
Menghitung biaya dan kapasitas: thread, bukan jumlah solve
Ini bagian yang paling sering salah dianggarkan tim otomatisasi di Indonesia. CaptchaAI menagih per thread bersamaan, bukan per solve, dan setiap paket memberi solve tanpa batas selama bulan berjalan. Pertanyaannya bukan "berapa CAPTCHA per hari", melainkan "berapa solve yang berjalan bersamaan saat puncak beban".
Contoh yang realistis untuk pasar lokal: sebuah agensi price-monitoring di Jakarta menjalankan 12 worker Node.js paralel di AWS ap-southeast-3. Puncaknya sekitar 12 solve in-flight, sehingga STANDARD ($30/bulan, 15 thread) sudah cukup. Kalau worker naik ke 40 saat musim promo, ADVANCE ($90/bulan, 50 thread) yang sesuai. Titik masuk terkecil adalah BASIC ($15/bulan, 5 thread); skala lebih besar tersedia hingga PREMIUM ($170/bulan, 100 thread). Harga selalu dalam USD.
Karena Turnstile selesai di bawah 10 detik, satu thread bisa menangani banyak solve berurutan dalam semenit — itulah alasan model thread biasanya lebih hemat untuk beban scraping yang bursty. Satu catatan kepatuhan: ambil hanya data yang memang boleh Anda proses, sesuai UU Pelindungan Data Pribadi (UU 27/2022).
Tabel gejala, penyebab, dan penanganan
| Gejala | Penyebab | Penanganan |
|---|---|---|
Sitekey diawali 6Le |
Itu reCAPTCHA, bukan Turnstile | Ganti ke method=userrecaptcha |
| Token ditolak server tujuan | Sitekey salah, atau token sudah kedaluwarsa | Ekstrak ulang sitekey, persingkat jeda antara solve dan submit |
| Sitekey tidak ditemukan | Widget dimuat lewat JavaScript, bukan HTML awal | Render halaman dengan Puppeteer atau Playwright |
ERROR_BAD_PARAMETERS |
sitekey atau pageurl tidak terkirim |
Pastikan keduanya ada dan tidak kosong |
| Respons 403 setelah submit | Header permintaan terlalu generik | Pakai User-Agent yang realistis dan konsisten |
Selalu CAPCHA_NOT_READY sampai batas waktu |
Thread akun sudah terpakai penuh | Turunkan konkurensi worker atau naikkan paket |
Kesalahan yang paling sering terjadi di lapangan
pageurltidak sama dengan halaman aslinya. Mengirim URL homepage padahal widget ada di/qa-loginmembuat token tidak valid meski solve sukses.- Menyelesaikan CAPTCHA terlalu awal. Kalau ada langkah lambat di antara solve dan submit, pindahkan langkah tersebut ke sebelum solve.
- API key ditulis langsung di kode. Simpan di environment variable, jangan di-commit.
Pertanyaan yang sering diajukan
Berapa lama Turnstile diselesaikan lewat CaptchaAI?
Waktu penyelesaian Turnstile berada di bawah 10 detik dengan tingkat keberhasilan yang tinggi pada tipe yang didukung. Karena itu jeda polling 5 detik seperti pada contoh sudah cukup; loop 30 iterasi hanya menjadi jaring pengaman.
Apakah saya butuh Puppeteer atau Playwright?
Tidak untuk alur di artikel ini — fetch bawaan Node.js sudah cukup selama sitekey terbaca dari HTML awal. Browser otomasi baru diperlukan ketika widget Turnstile disuntikkan sepenuhnya oleh JavaScript, sehingga HTML mentah tidak memuat data-sitekey sama sekali.
Berapa thread yang saya butuhkan untuk 20 worker paralel?
Hitung dari solve yang berjalan bersamaan, bukan dari total harian. Dua puluh worker yang masing-masing menahan satu solve berarti 20 thread, sehingga ADVANCE ($90/bulan, 50 thread) memberi ruang yang lega; STANDARD ($30/bulan, 15 thread) akan menjadi titik antre.
Bisakah satu token Turnstile dipakai ulang di beberapa request?
Tidak. Token bersifat sekali pakai dan berumur pendek, jadi setiap percobaan submit membutuhkan solve baru. Menyimpan token untuk dipakai belakangan adalah penyebab umum kegagalan yang sulit ditelusuri.
Ringkasan
Menyelesaikan Cloudflare Turnstile dari Node.js dengan CaptchaAI berarti tiga hal: ekstrak sitekey yang diawali 0x, kirim task dengan method=turnstile lalu polling hasilnya, dan pasang token pada field cf-turnstile-response. Bungkus alur itu dalam satu kelas solver, ambil API key dari environment variable, dan ukur kebutuhan paket dari jumlah solve bersamaan.