Token sudah ada di dalam textarea g-recaptcha-response, tetapi tombol kirim tetap abu-abu dan form menolak dikirim. Penyebabnya hampir selalu sama: token itu hanya data, sedangkan yang membuka form adalah fungsi callback yang biasanya dipanggil JavaScript Google setelah pengguna menyelesaikan tantangan. Kalau token disuntikkan dari luar, tidak ada yang memanggil fungsi itu — jadi Anda harus memanggilnya sendiri.
Artikel ini menunjukkan cara membaca nama callback dari DOM, memicunya dari Selenium dan Puppeteer, lalu menangani tiga kasus tepi yang paling sering membuat skrip gagal tanpa pesan error: situs tanpa data-callback, callback yang tersembunyi di dalam closure, dan expired-callback yang mengunci ulang form.
Kenapa token saja tidak cukup
Widget reCAPTCHA v2 mendeklarasikan nama callback-nya langsung di markup:
<div class="g-recaptcha"
data-sitekey="6Le-SITEKEY"
data-callback="onCaptchaSuccess"
data-expired-callback="onCaptchaExpired">
</div>
<script>
function onCaptchaSuccess(token) {
document.getElementById('submit-btn').disabled = false;
document.getElementById('captcha-token').value = token;
}
</script>
Ketika pengguna sungguhan menyelesaikan CAPTCHA, JavaScript Google memanggil onCaptchaSuccess(token) dengan string token sebagai satu-satunya argumen. Fungsi itulah yang mengaktifkan tombol, mengisi field tersembunyi, memvalidasi form, atau menembakkan permintaan AJAX.
Otomatisasi memotong jalur ini. CaptchaAI mengembalikan token yang valid, Anda menaruhnya di textarea — dan berhenti di situ. Halaman tidak pernah tahu ada yang berubah, karena menulis value pada textarea tidak memicu event apa pun. Urutan yang benar selalu dua langkah: isi token, lalu panggil callback.
Tiga cara menemukan nama callback
Cara 1: baca atribut data-callback
Ini kasus mayoritas dan paling murah untuk dicek. Jalankan di console browser:
// In browser console
const widget = document.querySelector('.g-recaptcha');
const callbackName = widget?.getAttribute('data-callback');
console.log('Callback:', callbackName);
Kalau hasilnya bukan null, Anda sudah selesai — nama itu adalah properti di window.
Cara 2: telusuri pemanggilan grecaptcha.render
Sebagian situs merender widget lewat JavaScript dan menaruh callback di objek opsi, bukan di atribut HTML. Atribut data-callback akan kosong padahal callback-nya ada:
// Search page source for grecaptcha.render
document.querySelectorAll('script:not([src])').forEach(s => {
if (s.textContent.includes('grecaptcha.render')) {
console.log(s.textContent.match(/callback\s*:\s*(\w+)/)?.[1]);
}
});
Cara 3: cegat pendaftaran callback
Kalau dua cara di atas gagal, bungkus grecaptcha.render sebelum halaman selesai memuat. Di DevTools, buat snippet baru pada tab Sources lalu jalankan:
const origRender = grecaptcha.render;
grecaptcha.render = function(container, params) {
console.log('Render callback:', params.callback);
console.log('Expired callback:', params['expired-callback']);
return origRender.apply(this, arguments);
};
Cara ini sekaligus membocorkan nama expired-callback, yang berguna untuk bagian kasus tepi di bawah.
Kalau Anda belum pernah mengirim satu task pun ke API CaptchaAI, kuasai dulu alur kirim-dan-polling-nya, lalu kembali ke sini untuk bagian callback.
Memicu callback dari Selenium (Python)
Alurnya tetap empat langkah baku: kirim task, simpan task ID, polling, pakai token. Yang ditambahkan di sini hanya langkah kelima, yaitu memanggil callback setelah token masuk. Perhatikan bahwa nama callback diambil dinamis dari DOM, bukan di-hardcode:
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")
# Extract sitekey and callback
sitekey = driver.find_element(
By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")
callback = driver.find_element(
By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-callback")
# Solve with CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": driver.current_url,
"json": "1",
}).json()
task_id = resp["request"]
token = None
for _ in range(24):
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["status"] == 1:
token = result["request"]
break
# Inject token into textarea
driver.execute_script("""
document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
""", token)
# Trigger the callback
if callback:
driver.execute_script(f"window['{callback}'](arguments[0]);", token)
print(f"Triggered callback: {callback}")
else:
# Fallback: try ___grecaptcha_cfg
driver.execute_script("""
try {
var widgetId = Object.keys(___grecaptcha_cfg.clients)[0];
var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
if (typeof callback === 'function') callback(arguments[0]);
} catch(e) {}
""", token)
print("Triggered callback via ___grecaptcha_cfg")
Cabang else memakai ___grecaptcha_cfg, objek konfigurasi internal grecaptcha. Itu jaring pengaman untuk callback yang didefinisikan di dalam closure sehingga tidak pernah muncul di window. Karena strukturnya internal dan bisa berubah kapan saja, bungkus selalu dengan try/catch dan jangan jadikan jalur utama.
Parameter pengiriman task-nya sendiri (method, googlekey, pageurl) tidak berubah dari alur reCAPTCHA v2 biasa; yang khas di artikel ini hanya langkah callback-nya.
Memicu callback dari Puppeteer (Node.js)
Pola yang sama di Node.js. Ada satu tambahan berguna: beberapa tema menyembunyikan textarea dengan display: none, dan sebagian skrip validasi form mengabaikan field tersembunyi, jadi textarea dikembalikan ke block sebelum callback dipanggil:
const puppeteer = require('puppeteer');
// After solving and getting the token...
await page.evaluate((token, callbackName) => {
// Set textarea value
const textarea = document.querySelector(
'textarea[name="g-recaptcha-response"]'
);
textarea.value = token;
textarea.style.display = 'block'; // sometimes hidden
// Trigger callback
if (callbackName && typeof window[callbackName] === 'function') {
window[callbackName](token);
console.log(`Called ${callbackName}()`);
} else {
// Fallback: search grecaptcha config
try {
const clients = ___grecaptcha_cfg.clients;
const widgetId = Object.keys(clients)[0];
const cb = clients[widgetId]?.aa?.l?.callback;
if (typeof cb === 'function') cb(token);
} catch (e) {}
}
}, token, callbackName);
Tabel diagnosis cepat
Empat gejala berikut menutup hampir semua kasus "token valid tetapi form tidak bergerak":
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Form tetap nonaktif setelah token disuntikkan | Callback tidak pernah dipanggil | Temukan namanya, lalu panggil manual |
ReferenceError: function not defined |
Callback didefinisikan di dalam closure | Pakai fallback ___grecaptcha_cfg |
| Token masuk tetapi AJAX tidak terkirim | Callback memicu AJAX, bukan submit form | Baca isi fungsi callback-nya |
| Token diterima lalu halaman menampilkan kesalahan | Token kedaluwarsa sebelum callback dipanggil | Selesaikan lebih dekat ke waktu submit |
Kasus tepi 1: situs tanpa data-callback
Sebagian situs memang tidak memakai callback. Mereka memanggil grecaptcha.getResponse() pada saat form dikirim dan membandingkan hasilnya. Di sini memanggil callback tidak ada gunanya — yang perlu diganti adalah getResponse itu sendiri:
driver.execute_script("""
const token = arguments[0];
document.querySelector('textarea[name="g-recaptcha-response"]').value = token;
// Override getResponse to return the token
if (typeof grecaptcha !== 'undefined') {
grecaptcha.getResponse = function() { return token; };
}
""", token)
# Then submit the form normally
driver.find_element(By.CSS_SELECTOR, "form").submit()
Setelah itu form dikirim seperti biasa, dan validasi di sisi klien membaca token yang Anda kembalikan.
Kasus tepi 2: expired-callback mengunci ulang form
Atribut data-expired-callback dipakai untuk menonaktifkan kembali tombol kirim saat token kedaluwarsa. Token reCAPTCHA v2 berlaku sekitar dua menit. Kalau skrip Anda menyuntikkan token yang sudah berumur satu setengah menit — misalnya karena polling berjalan lama atau ada langkah lain di antaranya — callback ini bisa menyala tepat sesudah callback sukses dan mengembalikan form ke kondisi terkunci.
Periksa dulu apakah situs mendefinisikannya:
// Check for expired callback
const expiredCallback = document.querySelector('.g-recaptcha')
?.getAttribute('data-expired-callback');
console.log('Expired callback:', expiredCallback);
Perbaikannya bukan menambah percobaan ulang, melainkan memperpendek jarak antara token diterima dan form dikirim: minta token sedekat mungkin ke langkah submit, bukan di awal alur.
Catatan latensi untuk tim di Indonesia
Umur token yang hanya dua menit membuat lokasi worker Anda ikut berpengaruh. Tim otomatisasi di Indonesia umumnya menjalankan worker di AWS ap-southeast-1 (Singapura), ap-southeast-3 (Jakarta), atau GCP asia-southeast2 (Jakarta) — bukan di laptop di balik koneksi rumah yang naik-turun. Bedanya terasa: satu siklus polling yang mundur beberapa detik karena jaringan lambat langsung memakan sisa umur token sebelum callback sempat dipanggil.
Dua kebiasaan yang membantu di lingkungan seperti ini:
- Jalankan pengiriman task dan pengambilan hasil dari region yang sama dengan browser otomatis Anda, supaya waktu polling tidak ditambah perjalanan lintas region.
- Uji urutan token-lalu-callback di halaman QA milik sendiri (
staging.example.com/qa-login) sebelum menjalankannya pada volume produksi.
Soal biaya, model CaptchaAI berbasis thread, bukan per solve: dari paket BASIC ($15/bulan, 5 thread) sampai VIP-3 ($7,500/bulan, 5.000 thread), dengan solve tanpa batas di setiap thread. Untuk pekerjaan borongan scraping dan monitoring harga yang lazim di pasar freelance lokal, biaya bulanan tetap lebih mudah dimasukkan ke penawaran klien daripada tagihan per solve yang ikut naik-turun mengikuti volume. Yang menentukan paket bukan jumlah solve, melainkan berapa banyak CAPTCHA yang Anda proses secara bersamaan.
Pola pemasangan token yang sama berlaku untuk tipe lain yang didukung: Cloudflare Turnstile dan GeeTest v3 juga mengembalikan token yang harus dipasang ke elemen yang tepat sebelum form menerimanya.
Pertanyaan Umum
Kenapa tombol kirim tetap mati padahal token sudah masuk ke textarea?
Karena mengubah value sebuah textarea tidak memicu event apa pun. Tombol diaktifkan oleh fungsi callback, jadi panggil fungsi itu secara eksplisit dengan token sebagai argumen.
Bagaimana cara tahu nama callback kalau data-callback kosong?
Cari pemanggilan grecaptcha.render di script inline halaman, atau bungkus grecaptcha.render lewat snippet DevTools sebelum halaman memuat untuk mencetak nama callback yang didaftarkan.
Apakah aman meng-hardcode nama callback di skrip?
Tidak disarankan. Di sebagian situs nama itu digenerate ulang setiap page load. Ekstrak dari DOM saat runtime supaya skrip tidak patah setelah deploy berikutnya.
Berapa lama token reCAPTCHA v2 berlaku sebelum callback perlu dipanggil?
Sekitar dua menit. Minta token sedekat mungkin ke waktu submit agar expired-callback tidak menyala dan mengunci ulang form.
Apakah reCAPTCHA v3 memakai mekanisme callback yang sama?
Tidak. reCAPTCHA v3 memakai grecaptcha.execute() yang mengembalikan sebuah Promise, tanpa widget yang terlihat dan tanpa atribut data-callback.
Selesaikan reCAPTCHA v2 dengan penanganan callback yang tepat menggunakan CaptchaAI
Ambil API key Anda di captchaai.com, lalu uji urutan token-plus-callback di halaman staging Anda sendiri.