Skrip automation yang berjalan mulus di halaman biasa sering gagal total begitu CAPTCHA muncul di dalam modal — penyebabnya hampir selalu sama: sitekey belum ada di HTML awal, baru dirender lewat JavaScript setelah modal dibuka. Kalau selector Anda mencari .g-recaptcha sebelum modal tampil, hasilnya selalu kosong dan solver Anda tidak punya apa pun untuk dikirim ke CaptchaAI.
Pola ini sangat akrab buat automation engineer dan freelancer scraping di Indonesia — banyak proyek dari Upwork atau Fastwork yang minta automasi login atau checkout, dan CAPTCHA modal baru muncul setelah tombol "Masuk" atau "Bayar" diklik, bukan saat halaman pertama kali dimuat. Panduan ini membahas cara mendeteksi modal begitu terbuka, mengekstrak sitekey dari DOM yang baru dirender, menyelesaikan CAPTCHA lewat CaptchaAI, dan menyuntikkan token kembali sebelum modal tertutup otomatis atau sesi pengguna kedaluwarsa.
Karena modal semacam ini biasanya membungkus form login atau checkout yang memuat data pribadi, pastikan automation Anda hanya memproses data yang memang berwenang Anda akses — sejalan dengan UU PDP (UU 27/2022).
Pola modal CAPTCHA yang paling sering ditemui
Sebelum menulis kode deteksi, kenali dulu pola modal yang paling sering memicu CAPTCHA muncul:
| Pola | Pemicu | Tantangan |
|---|---|---|
| Modal login | Klik tombol "Masuk" | CAPTCHA dirender di dalam overlay div |
| Interstisial anti-bot | Otomatis setelah pola akses mencurigakan | Modal layar penuh memblokir seluruh halaman |
| Konfirmasi checkout | Submit form pembayaran | Modal verifikasi muncul sebelum transaksi diproses |
| Dialog rate limit | Terlalu banyak permintaan dalam waktu singkat | Modal berisi gerbang CAPTCHA |
| Modal checkout saat flash sale | Volume checkout melonjak dalam waktu singkat | CAPTCHA modal muncul mendadak di tengah proses bayar, sering dengan timeout ketat |
| Persetujuan cookie + CAPTCHA | Kunjungan pertama | CAPTCHA tertanam di dalam dialog consent |
Implementasi Python: menangani CAPTCHA modal dengan Playwright
Kode berikut memicu modal, menunggu elemennya terlihat, mendeteksi jenis CAPTCHA di dalamnya (reCAPTCHA atau Turnstile), mengirim task ke CaptchaAI, lalu menyuntikkan token ke elemen yang tepat begitu solve selesai.
import requests
import time
from playwright.sync_api import sync_playwright
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
"""Submit and poll a CAPTCHA."""
params = {
"key": API_KEY,
"method": method,
"json": 1,
}
if method == "userrecaptcha":
params["googlekey"] = sitekey
params["pageurl"] = pageurl
elif method == "turnstile":
params["sitekey"] = sitekey
params["pageurl"] = 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 detect_modal_captcha(page):
"""
Detect CAPTCHA inside a visible modal/dialog.
Returns (sitekey, method) or (None, None).
"""
return page.evaluate("""
() => {
// Find visible modals
const modalSelectors = [
'.modal.show',
'.modal[style*="display: block"]',
'dialog[open]',
'[role="dialog"]:not([aria-hidden="true"])',
'.overlay.visible',
'.popup.active',
'[class*="modal"][class*="open"]',
];
let modal = null;
for (const sel of modalSelectors) {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
modal = el;
break;
}
}
// If no modal found, search entire document
const searchRoot = modal || document;
// Check for reCAPTCHA
const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
if (recaptcha) {
return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
}
// Check for Turnstile
const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
if (turnstile) {
return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
}
// Check for hCaptcha
const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
if (hcaptcha) {
return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
}
return null;
}
""")
def inject_token_in_modal(page, token, method="userrecaptcha"):
"""Inject token into the CAPTCHA inside the modal."""
if method == "userrecaptcha":
page.evaluate("""
(token) => {
// Find response textarea (may be inside modal)
const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
textareas.forEach(ta => {
ta.value = token;
ta.style.display = 'block';
});
// Trigger callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
Object.values(___grecaptcha_cfg.clients).forEach(client => {
Object.values(client).forEach(val => {
if (val && typeof val === 'object') {
Object.values(val).forEach(v => {
if (v && typeof v.callback === 'function') v.callback(token);
});
}
});
});
}
}
""", token)
elif method == "turnstile":
page.evaluate("""
(token) => {
const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
inputs.forEach(inp => { inp.value = token; });
if (typeof window.turnstileCallback === 'function') {
window.turnstileCallback(token);
}
}
""", token)
def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
"""
Full workflow: trigger modal, detect CAPTCHA, solve, inject.
"""
# Step 1: Trigger the modal if needed
if trigger_selector:
print(f"Clicking trigger: {trigger_selector}")
page.click(trigger_selector)
# Step 2: Wait for modal to become visible
print("Waiting for modal...")
modal_selectors = [
".modal.show",
"dialog[open]",
'[role="dialog"]:not([aria-hidden="true"])',
".popup.active",
]
modal_visible = False
for selector in modal_selectors:
try:
page.wait_for_selector(selector, timeout=timeout)
modal_visible = True
print(f" Modal detected: {selector}")
break
except Exception:
continue
if not modal_visible:
print(" No modal detected")
return None
# Step 3: Wait for CAPTCHA to render inside modal
time.sleep(2) # Brief pause for dynamic CAPTCHA loading
captcha_info = detect_modal_captcha(page)
if not captcha_info:
print(" No CAPTCHA found in modal")
return None
sitekey = captcha_info["sitekey"]
method = captcha_info["method"]
print(f" Found {method} CAPTCHA: {sitekey[:20]}...")
# Step 4: Solve via CaptchaAI
token = solve_captcha(sitekey, page.url, method)
print(f" Solved: {token[:30]}...")
# Step 5: Inject token
inject_token_in_modal(page, token, method)
print(" Token injected")
return token
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_load_state("networkidle")
# Handle CAPTCHA that appears in login modal
token = handle_modal_captcha(
page,
trigger_selector="button#login-btn",
timeout=10000,
)
if token:
# Fill form fields inside modal
page.fill('dialog input[name="email"]', "user@example.com")
page.fill('dialog input[name="password"]', "password123")
# Submit modal form
page.click('dialog button[type="submit"]')
page.wait_for_load_state("networkidle")
browser.close()
main()
Implementasi JavaScript: menangani modal dengan Puppeteer
Versi Puppeteer memakai alur yang sama persis — deteksi modal, ekstrak sitekey, submit ke in.php, polling res.php, lalu pengaturan token — hanya API browser-nya yang berbeda dari Playwright.
const puppeteer = require("puppeteer");
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", 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 waitForModal(page, timeout = 10000) {
const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
for (const sel of selectors) {
try {
await page.waitForSelector(sel, { visible: true, timeout });
return sel;
} catch {}
}
return null;
}
async function detectModalCaptcha(page) {
return page.evaluate(() => {
const checks = [
{ sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
{ sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
];
for (const { sel, method } of checks) {
const el = document.querySelector(sel);
if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
}
return null;
});
}
async function handleModalCaptcha(page, triggerSelector) {
// Trigger modal
if (triggerSelector) await page.click(triggerSelector);
// Wait for modal
const modalSel = await waitForModal(page);
if (!modalSel) { console.log("No modal found"); return null; }
console.log(`Modal visible: ${modalSel}`);
// Wait for CAPTCHA render
await new Promise((r) => setTimeout(r, 2000));
const info = await detectModalCaptcha(page);
if (!info) { console.log("No CAPTCHA in modal"); return null; }
console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
const token = await solveCaptcha(info.sitekey, page.url(), info.method);
console.log(`Solved: ${token.substring(0, 30)}...`);
// Inject token
await page.evaluate((t, method) => {
if (method === "userrecaptcha") {
document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
} else if (method === "turnstile") {
document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
}
}, token, info.method);
return token;
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://example.com", { waitUntil: "networkidle2" });
const token = await handleModalCaptcha(page, "button#login-btn");
if (token) {
await page.type('dialog input[name="email"]', "user@example.com");
await page.click('dialog button[type="submit"]');
await page.waitForNavigation();
}
await browser.close();
})();
Faktor waktu yang menentukan berhasil-tidaknya modal CAPTCHA
Modal punya jendela waktu yang jauh lebih ketat daripada CAPTCHA di halaman biasa. Empat faktor ini paling sering bikin solve yang sudah berhasil tetap gagal terpakai:
| Faktor | Dampak | Mitigasi |
|---|---|---|
| Modal auto-close | Modal bisa tertutup sebelum penyelesaian selesai | Mulai proses solve begitu CAPTCHA terdeteksi, jangan ditunda |
| Sesi berakhir saat menunggu | Sesi server kedaluwarsa selama CAPTCHA diproses | Jaga sesi tetap hidup dengan heartbeat di background |
| Delay render CAPTCHA di modal | Widget butuh 1–3 detik untuk muncul di dalam modal | Tunggu 2 detik setelah modal terlihat, baru ekstrak sitekey |
| Token kedaluwarsa saat isi form | Token basi kalau form masih diisi setelah token didapat | Isi field lain dulu, selesaikan CAPTCHA paling akhir |
Kalau automation Anda dijalankan dari region seperti AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta), maupun GCP asia-southeast2 (Jakarta), latensi jaringan ke sebagian besar situs Asia Tenggara biasanya sudah rendah, jadi anggaran waktu di atas hampir selalu cukup. Kalau instance Anda jalan dari region lain seperti US atau Eropa, tambahkan buffer 2–3 detik ekstra di setiap batas waktu modal. Koneksi mobile pengguna akhir juga membuat waktu render CAPTCHA lebih bervariasi, jadi timeout yang sedikit longgar lebih aman daripada yang terlalu ketat.
Pemecahan masalah yang paling sering muncul
Kalau modal ternyata membungkus CAPTCHA di dalam iframe, gabungkan langkah di atas dengan penanganan iframe: setelah modal terdeteksi, pindah dulu ke konteks iframe di dalamnya sebelum mengekstrak sitekey — CAPTCHA di dalam iframe bersarang di dalam modal tidak akan terbaca oleh selector biasa.
| Masalah | Penyebab | Solusi |
|---|---|---|
| Modal terdeteksi tapi CAPTCHA tidak ketemu | CAPTCHA dirender async setelah modal terbuka | Tambah waktu tunggu; pakai MutationObserver untuk mendeteksi widget baru |
| Token sudah diinjeksi tapi modal tidak menutup | Fungsi callback CAPTCHA tidak terpicu | Cari dan panggil callback CAPTCHA secara eksplisit |
| Modal menutup saat solve berjalan | Timer auto-dismiss modal | Nonaktifkan timeout modal lewat JS: clearTimeout() pada timer-nya |
| Sitekey berubah setiap kali modal dibuka | Modal membuat instance CAPTCHA baru tiap render | Selalu ambil sitekey langsung dari DOM modal, jangan pernah cache |
| Klik trigger tidak membuka modal | Elemen belum interaktif atau tertutup overlay lain | Pakai page.dispatchEvent atau tunggu elemen benar-benar bisa diklik |
Pertanyaan yang sering diajukan
Apakah CaptchaAI mendukung semua jenis CAPTCHA yang muncul di modal?
Tidak semua. CaptchaAI menyelesaikan reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, dan beberapa tipe lain — termasuk dua yang paling sering muncul di modal login dan checkout, reCAPTCHA v2 dan Turnstile. hCaptcha belum didukung. Kode deteksi di atas tetap mengenali hCaptcha di dalam modal, tapi Anda perlu layanan lain untuk menyelesaikannya.
Bagaimana mendeteksi modal yang terbuka otomatis tanpa event klik?
Pasang MutationObserver sebelum navigasi ke halaman, supaya observer sudah aktif begitu DOM berubah. Begitu elemen modal ditambahkan dan terlihat, observer akan trigger dan Anda bisa langsung menjalankan alur deteksi CAPTCHA tanpa menunggu klik apa pun.
Berapa lama waktu aman sebelum modal timeout saat CAPTCHA sedang diproses?
Tidak ada angka yang berlaku universal karena tiap situs mengatur timeout modal sendiri-sendiri, tapi pola yang aman: mulai proses solve dalam 1–2 detik setelah modal terdeteksi, dan siapkan jendela waktu sekitar 10–30 detik sebelum kebanyakan modal auto-close. Kalau solve CAPTCHA Anda biasanya lebih lama dari itu, jaga sesi tetap hidup lewat heartbeat alih-alih mengejar modal yang sudah keburu tertutup.
Isi kolom formulir dulu atau selesaikan CAPTCHA dulu?
Isi dulu. Selesaikan semua field form lain terlebih dahulu, baru selesaikan CAPTCHA paling akhir. Urutan ini memperpendek jeda antara token didapat dan form dikirim, sehingga risiko token kedaluwarsa jauh lebih kecil.
Apa bedanya menangani CAPTCHA di dalam modal dibanding di halaman biasa?
Bedanya ada di timing, bukan di cara menyelesaikan CAPTCHA-nya. Di halaman biasa, sitekey biasanya sudah ada saat DOM pertama kali dimuat. Di dalam modal, sitekey baru muncul setelah trigger diklik dan modal selesai dirender, jadi Anda perlu lapisan deteksi tambahan — menunggu modal, menunggu widget CAPTCHA — sebelum bisa memanggil API CaptchaAI seperti biasa.
Artikel terkait
- Menyelesaikan reCAPTCHA v2 callback lewat API
- Menangani reCAPTCHA v2 dan Turnstile di situs yang sama
- Menangani beberapa CAPTCHA dalam satu halaman
- Penanganan CAPTCHA di Shadow DOM
- Ekstraksi CAPTCHA iframe dari frame bertingkat
Langkah selanjutnya
Modal CAPTCHA tidak perlu bikin automation Anda macet di tengah jalan — ambil API key CaptchaAI Anda dan terapkan alur deteksi modal ini di script Anda sendiri.