Explainers

Panduan Deteksi Implementasi Cloudflare Turnstile

Sitekey yang salah adalah penyebab paling umum solve Turnstile gagal — bukan solver-nya yang bermasalah, tapi sitekey yang diambil dari tempat yang keliru. Sebelum memanggil API CaptchaAI, Anda perlu tahu persis bagaimana situs target menanam widget Turnstile: lewat atribut HTML langsung, panggilan turnstile.render() di JavaScript, atau widget yang baru dirender setelah interaksi pengguna. Panduan ini membedah ketiganya satu per satu, lengkap dengan kode deteksi siap pakai.


Kenapa Metode Penanaman Menentukan Cara Deteksi

Tim automation dan scraping di Indonesia — freelancer Upwork/Fastwork maupun tim data internal startup — sering menganggap deteksi Turnstile sebagai satu langkah generik: "cari cf-turnstile, ambil sitekey, kirim ke solver." Kenyataannya distribusinya timpang:

  • Sekitar sepertiga situs menaruh sitekey langsung di HTML statis.
  • Sisanya merender widget lewat JavaScript, kadang baru setelah pengguna klik tombol atau setelah panggilan API internal selesai.
  • requests.get() biasa selalu mengembalikan sitekey kosong pada kasus kedua — bukan karena Turnstile tidak ada, tapi karena belum dirender saat HTML diambil.

Konteks Lokal: Latensi dan Kepatuhan Data

Worker deteksi-dan-solve di AWS ap-southeast-1/ap-southeast-3 atau GCP asia-southeast2 sebaiknya beri jeda WebDriverWait sedikit lebih longgar — round-trip ke challenges.cloudflare.com dari region ini sedikit lebih panjang, meski Turnstile sendiri tetap selesai di bawah 10 detik dengan tingkat keberhasilan tinggi.

Kepatuhan: UU Pelindungan Data Pribadi (UU 27/2022) dan UU ITE membatasi pengambilan data pribadi tanpa dasar hukum yang sah. Batasi scraping pada data yang Anda punya wewenang memprosesnya — panduan ini bukan nasihat hukum.

Tiga Cara Situs Menanam Widget Turnstile

Situs menyematkan Turnstile dengan tiga cara, masing-masing memerlukan pendekatan deteksi berbeda:

Metode Cara Kerjanya Kesulitan Deteksi
HTML implisit <div class="cf-turnstile" data-sitekey="..."> di source halaman Mudah (HTML statis)
JavaScript eksplisit turnstile.render() dipanggil dalam skrip Sedang (parse JS)
Pemuatan dinamis Widget dimuat setelah aksi pengguna atau XHR Sulit (butuh eksekusi JS)

Kenali dulu skenario yang sering bikin parser meleset sebelum menulis detektor sendiri:

Skenario Sulit yang Sering Ditemui

Skenario Tantangan Solusi
Sitekey dalam file JS eksternal Tidak di HTML halaman Parse file JavaScript tertaut untuk pola sitekey
Sitekey dari respons API Dimuat setelah XHR call Monitor network request untuk sitekey dalam respons JSON
Beberapa widget Turnstile Sitekey berbeda di halaman yang sama Cocokkan sitekey dengan form spesifik yang disubmit
Turnstile di shadow DOM Tidak dapat diakses via selector reguler Gunakan shadowRoot.querySelector dalam konteks browser
Sitekey dirender sisi server Tertanam dalam variabel template Periksa tag <script> untuk objek konfigurasi
Turnstile di belakang autentikasi Tidak terlihat di halaman publik Autentikasi dulu, lalu deteksi

Kalau Deteksi Gagal atau Solve Ditolak

Gejala Penyebab Solusi
Tag skrip ditemukan tapi tidak ada sitekey JS API dirender dengan konfigurasi dari sumber lain Periksa semua file JS tertaut dan respons XHR
Sitekey salah diekstraksi Beberapa widget CAPTCHA di halaman Cocokkan sitekey dengan elemen form di sekitarnya
Deteksi berhasil tapi solve gagal Parameter action diperlukan untuk validasi Sertakan nilai data-action dalam request solve
Widget tidak ada di HTML awal Pemuatan dinamis setelah interaksi pengguna Gunakan Selenium/Puppeteer untuk rendering halaman penuh
Field cf-turnstile-response kosong Widget belum selesai dimuat Tunggu widget selesai dimuat

Cara 1: Deteksi Lewat HTML Statis

Kasus termudah: situs menanam <div class="cf-turnstile" data-sitekey="..."> langsung di source, jadi satu requests.get() plus regex sudah cukup — tanpa browser headless. Fungsi di bawah mengekstrak sitekey, mode widget, tema, dan action sekaligus:

import re
import requests

def detect_turnstile_html(url):
    """Detect Turnstile from static HTML."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
        "Accept": "text/html,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(url, headers=headers, timeout=15)
    html = response.text

    result = {
        "turnstile_found": False,
        "sitekey": None,
        "mode": None,
        "theme": None,
        "action": None,
        "script_loaded": False,
    }

    # Check for Turnstile script
    if "challenges.cloudflare.com/turnstile" in html:
        result["script_loaded"] = True

    # Check for widget container
    if "cf-turnstile" in html:
        result["turnstile_found"] = True

        # Extract sitekey
        sitekey_match = re.search(
            r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', html
        )
        if sitekey_match:
            result["sitekey"] = sitekey_match.group(1)

        # Extract mode
        if 'data-size="invisible"' in html:
            result["mode"] = "invisible"
        elif 'data-appearance="interaction-only"' in html:
            result["mode"] = "non-interactive"
        else:
            result["mode"] = "managed"

        # Extract theme
        theme_match = re.search(r'data-theme=["\'](\w+)["\']', html)
        if theme_match:
            result["theme"] = theme_match.group(1)

        # Extract action
        action_match = re.search(r'data-action=["\']([^"\']+)["\']', html)
        if action_match:
            result["action"] = action_match.group(1)

    return result


# Usage
info = detect_turnstile_html("https://staging.example.com/qa-login")
if info["turnstile_found"]:
    print(f"Sitekey: {info['sitekey']}")
    print(f"Mode: {info['mode']}")

Cara 2: Deteksi Lewat Pemanggilan turnstile.render()

Situs yang membangun form login dengan JavaScript sering merender Turnstile lewat kode, bukan atribut HTML — biasanya karena widget perlu dimunculkan setelah kondisi tertentu (misalnya setelah field email diisi). Kalau regex cf-turnstile di Cara 1 tidak menemukan apa pun, cek dulu apakah turnstile.render() muncul di file JS yang di-load halaman:

import re

def detect_turnstile_js_api(html):
    """Detect Turnstile from JavaScript render calls."""
    patterns = [
        # turnstile.render('#element', {sitekey: '...'})
        r"turnstile\.render\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*\{([^}]+)\}",
        # turnstile.render(element, {sitekey: '...'})
        r"turnstile\.render\s*\([^,]+,\s*\{([^}]+)\}",
    ]

    for pattern in patterns:
        match = re.search(pattern, html, re.DOTALL)
        if match:
            config_text = match.group(match.lastindex)

            # Extract sitekey from config object
            sitekey_match = re.search(
                r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", config_text
            )
            # Extract callback
            callback_match = re.search(
                r"callback\s*:\s*(\w+|function)", config_text
            )
            # Extract action
            action_match = re.search(
                r"action\s*:\s*['\"]([^'\"]+)['\"]", config_text
            )
            # Extract appearance
            appearance_match = re.search(
                r"appearance\s*:\s*['\"]([^'\"]+)['\"]", config_text
            )

            return {
                "found": True,
                "method": "javascript_api",
                "sitekey": sitekey_match.group(1) if sitekey_match else None,
                "callback": callback_match.group(1) if callback_match else None,
                "action": action_match.group(1) if action_match else None,
                "appearance": appearance_match.group(1) if appearance_match else None,
            }

    return {"found": False, "method": None}

Cara 3: Deteksi Widget yang Dimuat Dinamis

Kasus paling sulit: widget baru muncul setelah pengguna klik tombol, setelah XHR selesai, atau setelah bundle JavaScript yang di-minify jalan penuh. Di sini regex saja tidak cukup — Anda perlu browser headless yang benar-benar mengeksekusi JavaScript halaman, lalu memeriksa DOM setelah render selesai. Dua contoh berikut memakai Selenium (Python) dan Puppeteer (Node.js); pilih sesuai stack automation yang sudah Anda pakai.

Python (Selenium)

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import re

def detect_turnstile_dynamic(url):
    """Detect dynamically loaded Turnstile using Selenium."""
    options = webdriver.ChromeOptions()
    driver = webdriver.Chrome(options=options)

    try:
        driver.get(url)

        # Wait for page to fully load
        WebDriverWait(driver, 10).until(
            lambda d: d.execute_script("return document.readyState") == "complete"
        )

        result = {
            "turnstile_found": False,
            "sitekey": None,
            "iframe_present": False,
            "response_field": False,
        }

        # Check for Turnstile iframe
        iframes = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='challenges.cloudflare.com']")
        if iframes:
            result["turnstile_found"] = True
            result["iframe_present"] = True

        # Check for cf-turnstile container
        containers = driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
        for container in containers:
            sitekey = container.get_attribute("data-sitekey")
            if sitekey:
                result["turnstile_found"] = True
                result["sitekey"] = sitekey

        # Check for hidden response field
        response_fields = driver.find_elements(
            By.CSS_SELECTOR, "[name='cf-turnstile-response'], [name='g-recaptcha-response']"
        )
        if response_fields:
            result["response_field"] = True

        # Check page source for JS API render
        page_source = driver.page_source
        js_match = re.search(
            r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", page_source
        )
        if js_match and not result["sitekey"]:
            result["sitekey"] = js_match.group(1)
            result["turnstile_found"] = True

        return result

    finally:
        driver.quit()

Node.js (Puppeteer)

Versi Puppeteer melakukan hal yang sama, plus memantau response jaringan untuk menangkap URL script Turnstile — berguna kalau Anda ingin memverifikasi bahwa widget benar-benar termuat, bukan cuma container kosong:

const puppeteer = require("puppeteer");

async function detectTurnstileDynamic(url) {
  const browser = await puppeteer.launch({
    headless: "new",
    args: [],
  });

  const page = await browser.newPage();

  const result = {
    turnstileFound: false,
    sitekey: null,
    iframePresent: false,
    responseField: false,
    scriptUrl: null,
  };

  // Monitor network for Turnstile script
  page.on("response", (response) => {
    if (response.url().includes("challenges.cloudflare.com/turnstile")) {
      result.scriptUrl = response.url();
    }
  });

  await page.goto(url, { waitUntil: "networkidle2" });

  // Check for Turnstile container
  const sitekey = await page.evaluate(() => {
    const el = document.querySelector(
      ".cf-turnstile, [data-sitekey]"
    );
    return el ? el.getAttribute("data-sitekey") : null;
  });

  if (sitekey) {
    result.turnstileFound = true;
    result.sitekey = sitekey;
  }

  // Check for Turnstile iframe
  const iframes = await page.$$("iframe[src*='challenges.cloudflare.com']");
  if (iframes.length > 0) {
    result.turnstileFound = true;
    result.iframePresent = true;
  }

  // Check for response field
  const responseField = await page.$(
    "[name='cf-turnstile-response']"
  );
  result.responseField = !!responseField;

  await browser.close();
  return result;
}

detectTurnstileDynamic("https://staging.example.com/qa-login").then(console.log);

Gabungkan Ketiganya: Class Detector Serba Bisa

Di produksi, Anda jarang tahu di muka metode mana yang dipakai situs target. Class TurnstileDetector berikut menyatukan Cara 1 dan Cara 2 dalam satu objek — beri satu URL, dan class ini mencoba semua pola sitekey yang dikenal sekaligus melaporkan metode implementasi yang terdeteksi:

import re
import requests

class TurnstileDetector:
    """Detect Cloudflare Turnstile across all implementation methods."""

    TURNSTILE_SCRIPT = "challenges.cloudflare.com/turnstile"
    SITEKEY_PATTERNS = [
        r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
        r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"TURNSTILE_SITE_KEY\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
    ]

    def __init__(self, url, html=None):
        self.url = url
        self.html = html
        if not self.html:
            self._fetch()

    def _fetch(self):
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 Chrome/120.0.0.0",
            "Accept": "text/html,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
        }
        response = requests.get(self.url, headers=headers, timeout=15)
        self.html = response.text

    def detect(self):
        """Run all detection methods and return results."""
        return {
            "url": self.url,
            "turnstile_present": self.has_turnstile(),
            "sitekey": self.extract_sitekey(),
            "mode": self.detect_mode(),
            "implementation": self.detect_implementation(),
            "script_loaded": self.has_script(),
            "response_field": self.has_response_field(),
            "action": self.extract_action(),
            "theme": self.extract_theme(),
        }

    def has_turnstile(self):
        return (
            self.has_script()
            or "cf-turnstile" in self.html
            or self.extract_sitekey() is not None
        )

    def has_script(self):
        return self.TURNSTILE_SCRIPT in self.html

    def has_response_field(self):
        return "cf-turnstile-response" in self.html

    def extract_sitekey(self):
        for pattern in self.SITEKEY_PATTERNS:
            match = re.search(pattern, self.html)
            if match:
                return match.group(1)
        return None

    def detect_mode(self):
        if 'data-size="invisible"' in self.html or "size: 'invisible'" in self.html:
            return "invisible"
        if 'data-appearance="interaction-only"' in self.html:
            return "non-interactive"
        if "cf-turnstile" in self.html:
            return "managed"
        return "unknown"

    def detect_implementation(self):
        if "cf-turnstile" in self.html and re.search(r"data-sitekey=", self.html):
            return "html_implicit"
        if "turnstile.render" in self.html:
            return "javascript_explicit"
        if self.has_script() and not "cf-turnstile" in self.html:
            return "dynamic_loading"
        return "unknown"

    def extract_action(self):
        match = re.search(r'data-action=["\']([^"\']+)["\']', self.html)
        if match:
            return match.group(1)
        match = re.search(r"action\s*:\s*['\"]([^'\"]+)['\"]", self.html)
        return match.group(1) if match else None

    def extract_theme(self):
        match = re.search(r'data-theme=["\'](\w+)["\']', self.html)
        return match.group(1) if match else "auto"


# Usage
detector = TurnstileDetector("https://staging.example.com/qa-login")
info = detector.detect()

if info["turnstile_present"]:
    print(f"Sitekey: {info['sitekey']}")
    print(f"Mode: {info['mode']}")
    print(f"Implementation: {info['implementation']}")

Setelah Sitekey Didapat: Kirim ke CaptchaAI

Begitu sitekey dan pageurl sudah di tangan — dari metode mana pun di atas — langkah solve-nya sama persis: kirim ke in.php dengan method=turnstile, simpan task_id, lalu polling res.php sampai statusnya 1. Sertakan action kalau situs memvalidasinya (Anda sudah mengekstraknya di Cara 1 dan Cara 2):

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_detected_turnstile(detection_result):
    """Solve Turnstile using detection results."""
    if not detection_result["turnstile_present"]:
        raise ValueError("No Turnstile detected")

    if not detection_result["sitekey"]:
        raise ValueError("Sitekey not found — may need browser-based extraction")

    params = {
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": detection_result["sitekey"],
        "pageurl": detection_result["url"],
        "json": 1,
    }

    # Include action if present
    if detection_result.get("action"):
        params["action"] = detection_result["action"]

    submit = requests.post("https://ocr.captchaai.com/in.php", data=params)
    task_id = submit.json()["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("Turnstile solve timed out")


# Full workflow
detector = TurnstileDetector("https://example.com/signup")
info = detector.detect()

if info["turnstile_present"]:
    token = solve_detected_turnstile(info)
    print(f"Token: {token[:50]}...")

Pertanyaan Seputar Deteksi Turnstile

Berapa lama proses deteksi sampai token siap dipakai?

Deteksi HTML statis (Cara 1) hampir instan. Deteksi dinamis (Cara 3) menambah beberapa detik untuk rendering. Setelah sitekey didapat, solve Turnstile lewat CaptchaAI biasanya selesai di bawah 10 detik dengan tingkat keberhasilan tinggi.

Apakah deteksi bisa dilakukan tanpa Selenium atau Puppeteer?

Bisa, selama widget ada di HTML statis (Cara 1) atau di file JavaScript yang bisa diambil langsung dengan requests (Cara 2). Selenium/Puppeteer baru wajib kalau widget dirender lewat eksekusi JavaScript di browser.

Apa bedanya mode managed, non-interactive, dan invisible pada Turnstile?

Bedanya di seberapa banyak interaksi yang diminta ke pengunjung: managed bisa menampilkan checkbox, non-interactive menampilkan indikator tanpa klik, invisible tidak menampilkan apa pun. Ketiganya terdeteksi lewat atribut yang sama dan disolve dengan cara identik lewat CaptchaAI.

Apakah parameter action wajib disertakan saat solve?

Hanya kalau situs memvalidasinya di sisi server. Kalau data-action ada di HTML atau di config turnstile.render(), sertakan nilainya — mengabaikannya bisa membuat solve sukses tapi token ditolak backend situs.

Bagaimana jika sitekey situs berubah setelah scraper sudah jalan di produksi?

Operator situs bisa merotasi sitekey kapan saja tanpa pemberitahuan. Jangan hardcode sitekey — jalankan fungsi deteksi setiap kali sebelum solve, bukan sekali di awal lalu disimpan permanen.


Ringkasan

Urutan cek yang efisien untuk Turnstile apa pun:

  1. Container cf-turnstile dengan data-sitekey di HTML (Cara 1).
  2. Panggilan turnstile.render() di file JS yang di-load (Cara 2).
  3. Rendering penuh lewat Selenium/Puppeteer kalau keduanya nihil (Cara 3).

Class TurnstileDetector menjalankan cek pertama dan kedua otomatis dalam satu pemanggilan. Begitu sitekey didapat, solve dengan CaptchaAI Turnstile Solver — metode deteksi tidak memengaruhi hasil solve; semua mode widget ditangani dengan cara yang sama dan tingkat keberhasilan yang konsisten tinggi.

Artikel Terkait

Komentar dinonaktifkan untuk artikel ini.