Tutorials

CAPTCHA di modal pop-up: deteksi dan pengaturan token

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

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.

Komentar dinonaktifkan untuk artikel ini.