Satu aturan yang menyelamatkan banyak pipeline: jangan tetapkan penyedia CAPTCHA di awal skrip. Baca HTML halaman lebih dulu, tentukan apakah yang aktif reCAPTCHA v2 atau Cloudflare Turnstile, baru pilih method CaptchaAI yang sesuai dan isi field token yang benar. Skrip yang mengunci satu penyedia sejak baris pertama akan berhenti bekerja begitu situs target mengganti halaman checkout-nya — biasanya baru ketahuan setelah antrean gagal semalaman.
Di bawah ini pola deteksinya untuk Python dan Node.js, penanda HTML tiap penyedia, dan cara menghitung thread.
Alurnya: deteksi dulu, method belakangan
Pola di artikel ini selalu empat langkah, apa pun penyedianya:
- Ambil HTML halaman dan cari penanda kelas penyedia (
g-recaptchaataucf-turnstile), bukan sekadar atributdata-sitekey. - Kirim task ke
in.phpdenganmethodyang cocok —userrecaptchamemakai parametergooglekey,turnstilememakaisitekey. - Simpan task ID, lalu lakukan polling ke
res.phpsampai token siap. - Masukkan token ke field respons milik penyedia yang terdeteksi, bukan field yang ditulis mati di kode.
Langkah keempat yang paling sering terlewat. Token reCAPTCHA yang valid tetap ditolak kalau dikirim sebagai cf-turnstile-response, dan sebaliknya — karena itu contoh Python di bawah membawa nama field di dalam objek hasil deteksi.
Penanda HTML tiap penyedia
| Penyedia | Penanda HTML | URL skrip | Field respons |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
Baris hCaptcha ada di tabel karena Anda tetap perlu mengenalinya saat memindai halaman. CaptchaAI tidak mendukung hCaptcha maupun FunCaptcha (Arkose Labs), sedangkan GeeTest v4 baru berstatus segera hadir — catat halamannya, lalu lewati. Yang bisa ditangani lewat endpoint yang sama: reCAPTCHA v2 dan v3, Turnstile, Cloudflare Challenge, GeeTest v3, serta CAPTCHA gambar dan grid.
Kenapa satu situs bisa memakai dua penyedia
| Situasi | Yang Anda lihat di lapangan |
|---|---|
| Beda halaman, beda penyedia | Halaman login memakai reCAPTCHA, checkout memakai Turnstile |
| Uji A/B antar penyedia | URL yang sama menampilkan jenis berbeda dari sesi ke sesi |
| Migrasi yang belum selesai | Halaman lama masih reCAPTCHA, halaman baru sudah Turnstile |
| Failover otomatis | Penyedia utama gagal termuat, situs jatuh ke penyedia cadangan |
| Perbedaan wilayah | Pengunjung UE mendapat Turnstile karena pertimbangan GDPR |
Empat dari lima situasi ini tidak terlihat kalau Anda hanya menguji satu URL satu kali. Uji beberapa sesi pada waktu berbeda sebelum menyimpulkan target "hanya memakai reCAPTCHA".
Python: deteksi otomatis lalu kirim ke CaptchaAI
Skrip berikut menyatukan deteksi dan penyelesaian. Struktur CaptchaInfo membawa penyedia, method, sitekey, dan nama field respons sekaligus — tidak ada celah untuk salah pasang.
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://staging.example.com/qa-login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
Dua detail penting: regex memeriksa nama kelas penyedia dulu, lalu urutan atribut terbalik karena di HTML nyata data-sitekey kerap muncul sebelum class. Blok terakhir menangani reCAPTCHA yang dirender lewat grecaptcha.render().
JavaScript: versi ringkas untuk pipeline Node.js
Kalau pipeline Anda sudah berjalan di Node.js, pola yang sama muat dalam file yang jauh lebih pendek.
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://staging.example.com/qa-login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
Perhatikan jeda polling lima detik dengan batas 60 percobaan: aman untuk reCAPTCHA v2 (<60 detik) dan jauh lebih longgar untuk Turnstile (<10 detik). Lebih rapi lagi: batas waktu per penyedia, bukan satu batas global.
Skenario lokal: agensi pemantauan harga di Jakarta
Ambil contoh tim kecil di Jakarta yang menjalankan pemantauan harga untuk klien e-commerce — pekerjaan kontrak yang umum di komunitas otomatisasi Indonesia. Pipeline mereka berjalan di AWS ap-southeast-3 (Jakarta), ditulis dengan asumsi semua target memakai reCAPTCHA v2.
Begitu satu klien memindahkan checkout-nya ke Turnstile, worker malam mulai mengembalikan token yang ditolak — bukan kegagalan penyelesaian, melainkan token benar di field yang salah. Perbaikannya persis pola di atas: satu fungsi deteksi di depan, method yang dipilih saat runtime, dan nama field yang ikut dibawa bersama token.
Uji lebih dulu di lingkungan milik sendiri; URL staging.example.com/qa-login pada contoh kode bukan hiasan. Kalau data yang dikumpulkan menyentuh data pribadi, UU Pelindungan Data Pribadi (UU 27/2022) berlaku.
Menghitung thread saat dua penyedia bercampur
CaptchaAI menagih per thread bersamaan, bukan per penyelesaian, dengan penyelesaian tanpa batas selama bulan tagihan berjalan. Satu thread berarti satu CAPTCHA yang sedang diproses; begitu selesai, thread itu bebas menerima task berikutnya.
Yang berubah saat dua penyedia bercampur bukan harganya, melainkan lama thread tertahan: Turnstile melepas thread dalam kurang dari 10 detik, sedangkan reCAPTCHA v2 bisa menahannya hingga di bawah 60 detik.
Untuk skala freelance atau agensi kecil, BASIC ($15/bulan, 5 thread) biasanya cukup. STANDARD ($30/bulan, 15 thread) memberi ruang ketika halaman reCAPTCHA mendominasi, dan ADVANCE ($90/bulan, 50 thread) untuk banyak worker paralel. Tidak ada biaya tambahan per jenis CAPTCHA.
Masalah umum dan cara memperbaikinya
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Penyedia terdeteksi salah | Regex menangkap elemen lain | Cocokkan nama kelas penyedia dulu, jangan andalkan data-sitekey saja |
| Token valid tetapi ditolak form | Field respons tidak cocok dengan penyedianya | Pakai g-recaptcha-response untuk reCAPTCHA, cf-turnstile-response untuk Turnstile |
| Jenis CAPTCHA berganti antar kunjungan | Uji A/B atau pemilihan berbasis wilayah | Deteksi ulang tiap permintaan halaman, jangan simpan hasilnya sebagai cache |
| Dua penyedia terbaca sekaligus | Salah satu widget tersembunyi atau tidak aktif | Periksa visibilitas elemen, selesaikan hanya widget yang tampil |
| Deteksi kosong padahal widget terlihat | Widget dirender skrip, tidak ada di HTML awal | Cari grecaptcha.render() atau turnstile.render() di dalam tag skrip |
Pertanyaan umum
Bagaimana cara mendeteksi widget yang baru muncul setelah JavaScript berjalan?
HTML mentah dari requests tidak memuatnya. Cari pemanggilan grecaptcha.render() atau turnstile.render() di dalam tag skrip seperti contoh Python di atas, atau ambil HTML setelah render memakai browser otomatis lalu jalankan fungsi deteksi yang sama.
Apakah endpoint CaptchaAI berbeda untuk reCAPTCHA dan Turnstile?
Sama: in.php untuk mengirim task, res.php untuk polling. Yang berbeda hanya nilai method dan nama parameter sitekey-nya — userrecaptcha dengan googlekey, atau turnstile dengan sitekey.
Bisakah satu token dipakai ulang di halaman lain pada situs yang sama?
Tidak. Token terikat pada sitekey dan URL halaman saat task dikirim, dengan masa berlaku pendek. Halaman checkout ber-Turnstile tetap butuh task baru meski Anda baru menyelesaikan reCAPTCHA di login situs yang sama.
Apakah CaptchaAI bisa menyelesaikan hCaptcha kalau situs beralih ke sana?
Belum. hCaptcha dan FunCaptcha (Arkose Labs) tidak didukung, dan GeeTest v4 masih berstatus segera hadir. Jika fungsi deteksi Anda menemukan class="h-captcha", catat halamannya lalu lewati — jangan kirim task-nya ke in.php.
Berapa thread yang perlu saya siapkan untuk alur dua penyedia?
Hitung dari CAPTCHA yang berjalan bersamaan, bukan total harian. Kalau lima worker paralel masing-masing menunggu satu CAPTCHA, lima thread cukup — BASIC ($15/bulan, 5 thread). Tambah kapasitas kalau halaman reCAPTCHA v2 mendominasi.
Artikel terkait
- menyelesaikan reCAPTCHA v2 bertipe callback lewat API
- membedakan Cloudflare Challenge dan Turnstile
- perbandingan GeeTest dan Cloudflare Turnstile
Langkah berikutnya
Pasang deteksi otomatis sekali, dan situs dengan dua penyedia berhenti menjadi sumber kegagalan malam hari. Ambil API key CaptchaAI, jalankan skrip Python di atas pada satu halaman staging, lalu pastikan token mendarat di field yang tepat.
Panduan terkait: