Di SPA, sitekey reCAPTCHA tidak pernah ada di HTML pertama yang dikirim server, jadi yang perlu Anda ubah bukan cara menyelesaikan CAPTCHA melainkan kapan skrip Anda mencarinya. Tunggu widget selesai dirender, ambil sitekey dari DOM yang sudah hidup, kirim ke CaptchaAI seperti biasa, lalu kembalikan token ke tempat yang benar-benar dibaca aplikasi. Artikel ini menurunkan pola itu ke React, Vue, dan Angular, termasuk bagian yang paling sering gagal diam-diam: token yang sudah masuk lalu terhapus render ulang.
Empat langkah yang tidak berubah di SPA
Sisi API sama persis dengan halaman statis:
- Kirim sitekey dan URL halaman ke
in.php. - Simpan task ID dari respons.
- Polling
res.phpsampaistatusbernilai 1. - Pakai token yang kembali sebelum kedaluwarsa.
Yang berubah hanya dua hal di sisi browser: kapan sitekey bisa dibaca dan ke mana token dikembalikan. Hampir semua kegagalan integrasi SPA berhenti di salah satu titik itu, bukan di API.
Apa yang berbeda dari halaman biasa
| Halaman biasa | SPA |
|---|---|
| Skrip reCAPTCHA sudah ada di HTML pertama | Skrip disuntikkan setelah rute berubah |
| Widget dirender saat halaman dimuat | Widget dirender saat komponen dipasang |
| Sitekey terlihat di source halaman | Sitekey terselip di dalam bundle JavaScript |
| Form dikirim lewat POST biasa | Form dikirim lewat XHR/fetch ke endpoint REST |
Akibatnya, satu permintaan HTTP ke URL halaman tidak akan pernah menemukan sitekey — fase penemuan tetap butuh browser otomasi seperti Playwright atau Puppeteer.
Langkah 1: tunggu widget reCAPTCHA muncul
Widget sering baru dirender setelah tindakan tertentu: membuka modal login, menekan tombol lanjut, atau berpindah rute. Picu dulu tindakannya, baru tunggu iframe-nya.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://staging.example.com/qa-login")
# SPA may need a click or navigation to trigger reCAPTCHA
page.click("#show-login-form")
# Wait for reCAPTCHA iframe to appear
recaptcha_frame = page.wait_for_selector(
"iframe[src*='recaptcha']",
timeout=15000
)
print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))
Timeout 15 detik itu bukan angka hiasan. Banyak tim di Indonesia menjalankan otomasi dari AWS ap-southeast-1 (Singapura), ap-southeast-3 (Jakarta), atau GCP asia-southeast2 menuju origin yang jauh lebih barat, sementara bundle SPA kerap berukuran ratusan kilobyte. Dari koneksi seluler, naikkan batas waktu ke 20–30 detik dan tambahkan satu percobaan ulang.
Langkah 2: ambil sitekey dari DOM yang sudah hidup
Tiga metode berikut urut dari yang paling murah ke yang paling rapuh: src iframe, atribut data-sitekey, lalu isi tag <script> sebagai upaya terakhir.
# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None
# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
"[data-sitekey]",
"el => el.getAttribute('data-sitekey')"
)
# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
() => {
// Check for grecaptcha render calls
const scripts = document.querySelectorAll('script');
for (const s of scripts) {
const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
if (match) return match[2];
}
return null;
}
""")
Jalur ketiga patah begitu tim frontend mengganti bundler, jadi jangan menjadikannya andalan. Sitekey reCAPTCHA umumnya diawali 6L, dan pola itulah yang dipakai sebagai penyaring.
Langkah 3: kirim sitekey ke CaptchaAI
Mulai titik ini framework Anda tidak lagi relevan. Kirim method=userrecaptcha bersama googlekey dan pageurl, lalu polling sampai hasilnya siap.
import requests
import time
def solve_recaptcha(site_key, page_url):
# Submit task
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
# Poll for result
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("Solve timed out")
Satu detail yang sering terlewat: isi pageurl dengan URL rute tempat widget muncul, bukan URL root aplikasi — di SPA keduanya kerap berbeda meski tidak ada reload. Jeda 3 detik antar polling sudah cukup; memperkecilnya hanya menambah request kosong.
Langkah 4: kembalikan token ke tempat yang dibaca aplikasi
Di sinilah SPA benar-benar berbeda. Pada halaman statis, mengisi textarea g-recaptcha-response lalu menekan submit sudah cukup. Di React atau Vue, nilai itu bisa terhapus oleh render berikutnya, dan validator form membaca state komponen, bukan DOM.
Cara 1: isi textarea g-recaptcha-response
Paling langsung, dan layak dicoba lebih dulu karena banyak aplikasi hanya meneruskan isi textarea ke backend.
token = solve_recaptcha(site_key, "https://staging.example.com/qa-login")
# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
# Submit the form
page.click("#login-button")
Kalau form terkirim dan diterima server, berhenti di sini. Kalau tombol submit tetap mati, lanjut ke cara kedua.
Cara 2: panggil callback secara eksplisit
Cara ini paling tahan render ulang: aplikasi memperbarui state-nya sendiri, persis seperti saat pengguna sungguhan menyelesaikan CAPTCHA.
# Find the callback function name
callback = page.evaluate("""
() => {
const widget = document.querySelector('.g-recaptcha');
return widget?.getAttribute('data-callback') || null;
}
""")
# Call it with the token
if callback:
page.evaluate(f"window['{callback}']('{token}')")
else:
# Try the default grecaptcha callback
page.evaluate(f"""
if (window.grecaptcha) {{
// Set the response and trigger verification
document.getElementById('g-recaptcha-response').value = '{token}';
// Find and call any registered callbacks
const form = document.querySelector('form');
if (form) form.dispatchEvent(new Event('submit'));
}}
""")
Cara 3: mencegat XHR di Puppeteer
Kalau form dikirim lewat endpoint REST, pantau request-nya dan pastikan token sudah terpasang sebelum permintaan berangkat.
const puppeteer = require('puppeteer');
async function solveRecaptchaInSPA() {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// Intercept the API call and inject the token
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/login')) {
const postData = JSON.parse(request.postData() || '{}');
// Token already set via page.evaluate — let it pass
request.continue();
} else {
request.continue();
}
});
await page.goto('https://staging.example.com/qa-login');
await page.waitForSelector('[data-sitekey]');
const siteKey = await page.$eval(
'[data-sitekey]',
el => el.getAttribute('data-sitekey')
);
// Solve with CaptchaAI (implementation omitted — same as Python)
const token = await solveCaptcha(siteKey, page.url());
// Inject token and trigger callback
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
const callback = document.querySelector('.g-recaptcha')
?.getAttribute('data-callback');
if (callback && window[callback]) {
window[callback](t);
}
}, token);
}
Pola per framework
Perbedaan cara merender menentukan selector mana yang layak Anda tunggu.
React (react-google-recaptcha)
Aplikasi React umumnya memakai react-google-recaptcha, yang merender komponen secara asinkron.
# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")
# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
".g-recaptcha", "el => el.dataset.sitekey"
)
Vue (vue-recaptcha)
Vue sering membungkus widget dengan v-if, jadi elemennya baru ada setelah kondisi terpenuhi.
# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")
Angular
Angular kerap memuat modul reCAPTCHA secara lazy, jadi tunggu komponennya, bukan sekadar iframe.
# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")
Menangani perubahan rute
Navigasi di SPA terjadi tanpa reload penuh: URL berganti, halaman tidak.
# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")
# Navigate within the SPA
page.click("a[href='/login']")
# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)
Jebakan umumnya: menyimpan token lintas rute. Token reCAPTCHA berumur pendek dan sekali pakai, sedangkan widget di rute baru adalah instance baru — selesaikan ulang, jangan didaur ulang.
Biaya dan thread untuk alur SPA
Alur SPA memakai satu thread per CAPTCHA yang sedang dikerjakan; waktu tunggu browser Anda tidak ikut dihitung. Agensi price-monitoring kecil di Jakarta dengan lima worker Playwright paralel dan satu reCAPTCHA per sesi login sudah tercukupi paket BASIC ($15/bulan, 5 thread); tim yang naik ke belasan browser biasanya pindah ke STANDARD ($30/bulan, 15 thread). Karena penagihan berbasis thread dengan solve tanpa batas per thread, menambah rute ber-CAPTCHA tidak mengubah tagihan bulanan — yang mengubahnya hanya jumlah penyelesaian yang berjalan bersamaan.
Catatan kepatuhan singkat: jalankan otomasi seperti ini pada aplikasi milik sendiri atau staging yang Anda kelola, dan di bawah UU 27/2022 tentang Pelindungan Data Pribadi, jangan memproses data pribadi yang tidak berhak Anda ambil.
Pemecahan masalah
| Gejala | Penyebab | Perbaikan |
|---|---|---|
Textarea g-recaptcha-response tidak ditemukan |
Komponen SPA belum merender widget | Pakai wait_for_selector dengan batas waktu yang memadai |
| Token masuk, tetapi form tidak terkirim | Callback aplikasi tidak pernah dipanggil | Cari nilai data-callback lalu panggil fungsinya |
| Sitekey tidak ada di source halaman | Sitekey ada di bundle JS atau dimuat belakangan | Tunggu widget dirender, lalu ambil dari src iframe |
| Token berhasil sekali lalu gagal | Komponen dirender ulang dan nilainya hilang | Suntikkan token tepat sebelum pengiriman form |
| Nilai textarea kembali kosong sendiri | Reaktivitas React/Vue menimpa DOM | Gunakan jalur data-callback, bukan pengisian textarea |
Pertanyaan yang sering muncul
Berapa lama token reCAPTCHA tetap valid setelah diterima?
Sekitar dua menit dan sekali pakai — itu batas dari Google, bukan dari layanan solver. Pada alur SPA yang panjang, suntikkan token tepat sebelum request pengiriman, bukan segera setelah polling selesai.
Apakah pola yang sama berlaku untuk reCAPTCHA v3 di SPA?
Ya. CaptchaAI mendukung reCAPTCHA v3 dan reCAPTCHA v3 Enterprise. Bedanya, v3 tidak punya widget yang bisa ditunggu: sitekey diambil dari pemanggilan grecaptcha.execute di dalam bundle, dan nilai action dikirim bersama task.
Berapa thread yang dibutuhkan kalau satu halaman memuat dua widget?
Satu thread untuk setiap CAPTCHA yang diselesaikan pada saat yang sama. Dua widget yang ditangani berurutan tetap memakai satu thread; dua yang dikirim bersamaan memakai dua. Paket BASIC ($15/bulan) menyediakan 5 thread dengan solve tanpa batas per thread.
Token sudah masuk, tetapi tombol submit tetap tidak aktif. Kenapa?
Karena validasi form membaca state komponen, bukan DOM. Mengisi textarea tidak memicu event apa pun di React maupun Vue, jadi aplikasi menganggap CAPTCHA belum selesai. Panggil data-callback seperti pada Cara 2.
Bisakah CAPTCHA di SPA diselesaikan tanpa browser sama sekali?
Bisa, setelah Anda mengetahui sitekey dan endpoint REST yang dipakai form. Begitu keduanya terdokumentasi, klien HTTP biasa sudah cukup; fase penemuannya tetap perlu browser satu kali.
Artikel Terkait
- cara menyelesaikan callback reCAPTCHA v2 lewat API
- menangani Turnstile dan reCAPTCHA v2 di satu situs
- cara kerja mekanisme callback reCAPTCHA v2
Langkah berikutnya
Widget yang muncul belakangan bukan alasan otomasi Anda berhenti — ambil API key CaptchaAI dan selesaikan reCAPTCHA di rute SPA pertama Anda.