Chèn token vào g-recaptcha-response mà form vẫn không submit? Nhiều khả năng trang bạn đang test dùng reCAPTCHA v2 dạng callback — nó không đọc field ẩn đó, mà đợi bạn gọi thẳng một hàm JavaScript kèm theo token. Lệnh gọi tới API CaptchaAI thì y hệt bản tiêu chuẩn; thứ duy nhất khác là bước cuối, chỗ bạn giao token cho trang.
Bài này đi thẳng vào ba việc: nhận diện trang đang dùng callback, lấy token qua API CaptchaAI, và gọi đúng hàm để trang xử lý tiếp (submit form, xác thực, chuyển hướng…).
Chưa quen reCAPTCHA v2? XemCách giải reCAPTCHA v2 bằng APIcho luồng tiêu chuẩn trước, rồi quay lại đây cho biến thể callback.
Chuẩn bị trước khi bắt đầu
| Cần gì | Chi tiết |
|---|---|
| API key CaptchaAI | Lấy tạicaptchaai.com/api.php — chuỗi 32 ký tự. |
| URL trang đích | URL đầy đủ nơi widget reCAPTCHA v2 được load. |
| Sitekey reCAPTCHA v2 | Khóa công khai gắn với widget đó. |
| Công cụ automation trình duyệt | Selenium, Puppeteer hoặc Playwright — cần chạy được JavaScript để gọi hàm callback. |
| Tên hàm callback | Hàm JavaScript mà trang mong đợi nhận token. |
Hai bối cảnh quen thuộc với đội automation/QA ở Việt Nam hay đụng đúng biến thể callback này:
- Team outsource QA/automation (kiểu FPT Software, VNG hoặc startup TP.HCM/Hà Nội) kiểm thử login flow trên staging cho khách hàng nước ngoài — widget reCAPTCHA thường được nhúng qua SDK bên thứ ba, không phải cấu hình mặc định của Google, nên hay rơi vào dạng callback.
- Nhóm scraping nội bộ theo dõi giá trên các sàn thương mại điện tử gắn reCAPTCHA v2 callback ở form đăng nhập/đăng ký để chống bot.
Cả hai đều cần phân biệt đúng callback với chuẩn trước khi viết code, nếu không script sẽ chạy mà không báo lỗi nhưng form vẫn không submit.
Cách nhận diện trang dùng callback
reCAPTCHA v2 tiêu chuẩn ghi token vào textarea ẩn g-recaptcha-response. Bản callback bỏ qua field đó và gọi thẳng một hàm JavaScript.
Ba chỗ cần soát, theo thứ tự nhanh nhất trước:
- Thuộc tính
data-callbacktrên div widget (nhanh nhất, chỉ cần xem source). - Tham số
callbacktrong lệnh gọigrecaptcha.render()(khi widget được tạo bằng JS thay vì HTML tĩnh). - Cấu hình nội bộ
___grecaptcha_cfg.clients(dùng khi hai cách trên không thấy gì, ví dụ trang đã minify).
| Cách kiểm tra | Tốc độ | Khi nào cần |
|---|---|---|
data-callback |
Nhanh nhất — chỉ cần xem source | Widget dựng bằng HTML tĩnh |
grecaptcha.render() |
Nhanh — tìm trong file JS | Widget dựng động bằng JavaScript |
___grecaptcha_cfg.clients |
Chậm hơn — phải duyệt console | Hai cách trên không ra kết quả, hoặc trang đã minify |
Cách 1: Kiểm tra thuộc tính data-callback
Kiểm tra div tiện ích reCAPTCHA trong nguồn trang:
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
data-callback="SubmitToken">
</div>
Nếu data-callback xuất hiện, trang đang dùng callback. Giá trị (SubmitToken) chính là tên hàm bạn cần.
- Đây là cách nhanh nhất — không cần chạy JavaScript, chỉ cần "View Page Source".
- Nếu không thấy thuộc tính này, chuyển sang Cách 2.
Cách 2: Kiểm tra lệnh gọi grecaptcha.render()
Tìm grecaptcha.render trong JavaScript của trang:
grecaptcha.render('recaptcha-container', {
sitekey: '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
callback: userVerified
});
Thuộc tính callback chính là tên hàm. Ở ví dụ này là userVerified.
Cách này áp dụng khi widget được grecaptcha.render() dựng động thay vì khai báo sẵn trong HTML — thường gặp ở single-page app render widget bằng React/Vue.
Cách 3: Đọc cấu hình reCAPTCHA nội bộ
Mở console trình duyệt trên trang đích và chạy:
___grecaptcha_cfg.clients[0]
Duyệt cây đối tượng để tìm thuộc tính callback. Đường dẫn chính xác đổi theo từng trang — có thể là clients[0].aa.l.callback hoặc khác, tùy phiên bản reCAPTCHA và mức độ minify.
Nếu trang có nhiều widget, kiểm tra thêm clients[1], clients[2], v.v. — mỗi widget đăng ký callback riêng trong cùng object clients.
Script phát hiện nhanh
Chạy đoạn này trong console để tự động dò tên hàm callback:
// Check data-callback attributes
document.querySelectorAll('[data-callback]').forEach(el => {
console.log('data-callback:', el.getAttribute('data-callback'));
});
// Check internal config
if (typeof ___grecaptcha_cfg !== 'undefined') {
Object.keys(___grecaptcha_cfg.clients).forEach(key => {
const client = ___grecaptcha_cfg.clients[key];
console.log(`Client ${key}:`, JSON.stringify(client, null, 2));
});
}
Callback khác gì reCAPTCHA v2 tiêu chuẩn
Lệnh gọi tới API CaptchaAI giống hệt nhau. Khác biệt duy nhất nằm ở bước cuối — bạn làm gì với token sau khi nhận được.
| Bước | v2 tiêu chuẩn | v2 callback |
|---|---|---|
| 1. Gửi task tới CaptchaAI | method=userrecaptcha + sitekey + pageurl |
Giống nhau |
| 2. Polling kết quả | action=get + captcha ID |
Giống nhau |
| 3. Nhận token | Cùng định dạng token | Giống nhau |
| 4. Đưa token vào trang | Set giá trị field g-recaptcha-response |
Gọi hàm callback kèm token |
| 5. Submit form | Kích hoạt submit thủ công | Thường tự động — callback lo phần này |
Lưu ý quan trọng: Đừng set
g-recaptcha-responsetrên trang dùng callback. Trang bỏ qua field đó và chỉ chờ hàm callback được gọi. Set field mà không gọi hàm sẽ khiến trang xử sự như thể CAPTCHA chưa bao giờ được giải.
Vì sao CaptchaAI xử lý tốt trường hợp này
| Yếu tố | Chi tiết |
|---|---|
| Cùng một lệnh gọi API | Luồng submit/polling giống hệt reCAPTCHA v2 tiêu chuẩn — không cần tham số thêm |
| Tỷ lệ giải thành công | Cao trên reCAPTCHA v2 (callback và tiêu chuẩn dùng chung một solver) |
| Thời gian giải | Thường dưới 60 giây |
| Token tương thích cả hai kiểu | Token trả về dùng được cho cả g-recaptcha-response lẫn gọi callback |
| Giá | Gói theo thread, thấp nhất là BASIC ($15/tháng, 5 thread), không giới hạn số lần giải trong mỗi thread |
Token CaptchaAI trả về giống nhau dù trang triển khai reCAPTCHA v2 theo cách nào — điểm mấu chốt cần nhớ trước khi vào code: khác biệt chỉ nằm ở phần client-side, không phải ở cách gọi API.
Sơ đồ luồng xử lý
Toàn bộ quy trình gói gọn trong một vòng submit → poll → invoke, không có bước rẽ nhánh nào riêng cho callback:
Page → extract sitekey + pageurl + callback name
↓
POST to in.php (method=userrecaptcha)
↓
receive captcha ID
↓
wait 15–20 seconds
↓
GET res.php (action=get, id=…)
↓ ↓
CAPCHA_NOT_READY status=1 → token
(wait 5s, retry) ↓
invoke callback(token)
↓
site processes token automatically
Code Python (Selenium)
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
API_KEY = "YOUR_CAPTCHAAI_API_KEY"
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
PAGE_URL = "https://staging.example.com/qa-login"
CALLBACK_NAME = "SubmitToken" # The callback function name from the page
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
def solve_recaptcha_v2(api_key, sitekey, pageurl):
"""Submit a reCAPTCHA v2 task and return the solved token."""
# Step 1: Submit the captcha
submit_resp = requests.post(
SUBMIT_URL,
data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
},
timeout=30,
)
submit_resp.raise_for_status()
submit_data = submit_resp.json()
if submit_data.get("status") != 1:
raise RuntimeError(f"Submit failed: {submit_data}")
captcha_id = submit_data["request"]
print(f"Task created — captcha ID: {captcha_id}")
# Step 2: Wait before first poll
time.sleep(15)
# Step 3: Poll for result
for _ in range(60):
result_resp = requests.get(
RESULT_URL,
params={
"key": api_key,
"action": "get",
"id": captcha_id,
"json": 1,
},
timeout=30,
)
result_resp.raise_for_status()
result_data = result_resp.json()
if result_data.get("request") == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result_data.get("status") == 1:
return result_data["request"]
raise RuntimeError(f"Polling error: {result_data}")
raise TimeoutError("reCAPTCHA v2 solve timed out")
def detect_callback_name(driver):
"""Detect the reCAPTCHA callback function name from the page."""
# Try data-callback attribute first
callback = driver.execute_script("""
const el = document.querySelector('[data-callback]');
if (el) return el.getAttribute('data-callback');
return null;
""")
if callback:
return callback
# Try internal reCAPTCHA config
callback = driver.execute_script("""
if (typeof ___grecaptcha_cfg === 'undefined') return null;
const clients = ___grecaptcha_cfg.clients;
for (const key of Object.keys(clients)) {
const client = clients[key];
// Walk the object tree to find a callback function
const json = JSON.stringify(client);
const match = json.match(/"callback":"(\\w+)"/);
if (match) return match[1];
}
return null;
""")
return callback
# Main workflow
driver = webdriver.Chrome()
driver.get(PAGE_URL)
# Detect the callback name (or use the known name)
detected = detect_callback_name(driver)
callback_name = detected or CALLBACK_NAME
print(f"Using callback: {callback_name}")
# Solve the CAPTCHA
token = solve_recaptcha_v2(API_KEY, SITEKEY, PAGE_URL)
print(f"Solved token: {token[:80]}...")
# Invoke the callback with the token
driver.execute_script(f"{callback_name}(arguments[0]);", token)
print("Callback invoked — site should process the token automatically")
# Wait for the page to process
time.sleep(3)
driver.quit()
Code này làm gì:
- Gửi sitekey và pageurl tới
in.phpvớimethod=userrecaptcha— y hệt bản v2 tiêu chuẩn. - Polling
res.phpmỗi 5 giây cho tới khi có token. - Dò tên hàm callback trực tiếp từ DOM của trang.
- Gọi hàm callback đó kèm token vừa nhận, qua
execute_script. - JavaScript của chính trang xử lý phần còn lại — submit form, xác thực hoặc chuyển hướng.
Code Node.js (Puppeteer)
Cùng bốn bước như bản Python, chuyển sang async/await và fetch:
const puppeteer = require("puppeteer");
const API_KEY = "YOUR_CAPTCHAAI_API_KEY";
const SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-";
const PAGE_URL = "https://staging.example.com/qa-login";
const CALLBACK_NAME = "SubmitToken";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function solveRecaptchaV2(apiKey, sitekey, pageurl) {
// Step 1: Submit the captcha
const submitResp = await fetch(SUBMIT_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
key: apiKey,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: "1",
}),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) {
throw new Error(`Submit failed: ${JSON.stringify(submitData)}`);
}
const captchaId = submitData.request;
console.log(`Task created — captcha ID: ${captchaId}`);
// Step 2: Wait before first poll
await sleep(15_000);
// Step 3: Poll for result
for (let i = 0; i < 60; i++) {
const resultResp = await fetch(
`${RESULT_URL}?${new URLSearchParams({
key: apiKey,
action: "get",
id: captchaId,
json: "1",
})}`
);
const resultData = await resultResp.json();
if (resultData.request === "CAPCHA_NOT_READY") {
await sleep(5_000);
continue;
}
if (resultData.status === 1) {
return resultData.request;
}
throw new Error(`Polling error: ${JSON.stringify(resultData)}`);
}
throw new Error("reCAPTCHA v2 solve timed out");
}
async function detectCallbackName(page) {
return page.evaluate(() => {
// Try data-callback attribute
const el = document.querySelector("[data-callback]");
if (el) return el.getAttribute("data-callback");
// Try internal config
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
for (const key of Object.keys(clients)) {
const json = JSON.stringify(clients[key]);
const match = json.match(/"callback":"(\w+)"/);
if (match) return match[1];
}
}
return null;
});
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto(PAGE_URL, { waitUntil: "networkidle2" });
// Detect callback
const detected = await detectCallbackName(page);
const callbackName = detected || CALLBACK_NAME;
console.log(`Using callback: ${callbackName}`);
// Solve the CAPTCHA
const token = await solveRecaptchaV2(API_KEY, SITEKEY, PAGE_URL);
console.log(`Solved token: ${token.slice(0, 80)}...`);
// Invoke the callback
await page.evaluate(
(name, tkn) => {
window[name](tkn);
},
callbackName,
token
);
console.log("Callback invoked — site should process the token automatically");
await sleep(3_000);
await browser.close();
})();
So với bản Python, logic giống hệt — chỉ khác cú pháp:
fetchthay chorequests,await sleep()thay chotime.sleep().page.evaluate()thay chodriver.execute_script()để dò callback và gọi hàm.- Puppeteer chạy
headless: falsetrong ví dụ này để dễ quan sát khi debug; đổi thànhtruecho môi trường CI/staging.
Code PHP
Lệnh gọi API trong PHP giống hệt hai ngôn ngữ trên, nhưng có một giới hạn cần lưu ý trước khi dùng.
Gọi callback cần ngữ cảnh trình duyệt (DOM + JS runtime), nên ví dụ dưới đây chỉ xử lý phần lấy token phía server. Dùng một công cụ headless (ví dụ: PHP WebDriver) cho bước gọi hàm.
<?php
$apiKey = "YOUR_CAPTCHAAI_API_KEY";
$sitekey = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-";
$pageurl = "https://staging.example.com/qa-login";
// Step 1: Submit
$submit = file_get_contents("https://ocr.captchaai.com/in.php?" . http_build_query([
"key" => $apiKey,
"method" => "userrecaptcha",
"googlekey" => $sitekey,
"pageurl" => $pageurl,
"json" => 1,
]));
$submitData = json_decode($submit, true);
if ($submitData["status"] !== 1) {
die("Submit failed: " . $submit);
}
$captchaId = $submitData["request"];
echo "Task created — captcha ID: $captchaId\n";
// Step 2: Wait and poll
sleep(15);
for ($i = 0; $i < 60; $i++) {
$result = file_get_contents("https://ocr.captchaai.com/res.php?" . http_build_query([
"key" => $apiKey,
"action" => "get",
"id" => $captchaId,
"json" => 1,
]));
$resultData = json_decode($result, true);
if ($resultData["request"] === "CAPCHA_NOT_READY") {
sleep(5);
continue;
}
if ($resultData["status"] === 1) {
$token = $resultData["request"];
echo "Solved token: " . substr($token, 0, 80) . "...\n";
// Pass $token to your browser automation to invoke the callback
break;
}
die("Polling error: " . $result);
}
Có token từ PHP rồi, dùng công cụ automation trình duyệt (ví dụ: php-webdriver) để chạy:
SubmitToken("TOKEN_FROM_CAPTCHAAI");
Debug các tình huống thực tế
Trước khi tra bảng lỗi nhanh, đây là cách xử lý từng tình huống cụ thể:
Có token nhưng trang không phản ứng gì
Nguyên nhân phổ biến nhất: bạn set g-recaptcha-response thay vì gọi callback.
Kiểm tra hai chỗ, theo đúng thứ tự đã nêu ở phần nhận diện phía trên:
- Widget có
data-callbackkhông? grecaptcha.render()có tham sốcallbackkhông?
Nếu có, bạn bắt buộc phải gọi đúng hàm đó — set field sẽ bị bỏ qua hoàn toàn.
ReferenceError: SubmitToken is not defined
Hàm callback chưa được load hoặc bạn đang dùng sai tên. Thử theo thứ tự:
- Xác nhận lại tên qua
data-callbackhoặc cấu hình nội bộ. - Đợi trang load xong hẳn rồi mới gọi.
- Trên site đã minify, hàm có thể được gán cho một biến khác — kiểm tra
window.SubmitTokentrong console.
Token chạy tốt trên v2 tiêu chuẩn nhưng fail ở trang này
Nhiều khả năng đây là bản callback. Làm lạicác bước nhận diệnở trên để xác nhận, rồi chuyển sang gọi callback thay vì set field.
Trang có nhiều widget reCAPTCHA cùng lúc
Mỗi widget có thể gắn callback riêng. Soát từng div g-recaptcha, hoặc duyệt ___grecaptcha_cfg.clients để liệt kê hết các instance đã đăng ký, rồi khớp đúng widget với form bạn đang target.
Hai lỗi API không đặc thù cho callback
Hai mã lỗi này xuất hiện y hệt nhau trên cả reCAPTCHA v2 tiêu chuẩn lẫn callback — không liên quan đến cách bạn đưa token vào trang:
| Mã lỗi | Nguyên nhân | Cách xử lý |
|---|---|---|
ERROR_BAD_TOKEN_OR_PAGEURL |
Cặp sitekey/pageurl sai | Trích xuất lại cả hai giá trị từ trang |
ERROR_CAPTCHA_UNSOLVABLE |
Challenge không giải được | Thử lại với request mới |
Lỗi hay gặp khi xử lý callback
Bảng tra nhanh sáu lỗi phổ biến nhất, kèm cách sửa:
| # | Lỗi | Hậu quả | Cách sửa |
|---|---|---|---|
| 1 | Set g-recaptcha-response thay vì gọi callback |
Trang bỏ qua token — form không bao giờ submit | Tìm tên hàm callback và gọi nó kèm token |
| 2 | Sai tên hàm callback | JavaScript báo lỗi: hàm không tồn tại | Soát lại data-callback, grecaptcha.render() hoặc cấu hình nội bộ |
| 3 | Callback nằm ở client index khác | Nhắm nhầm widget trên trang có nhiều reCAPTCHA | Kiểm tra thêm ___grecaptcha_cfg.clients[1], clients[2], v.v. |
| 4 | Gọi callback trước khi trang sẵn sàng | Hàm chưa tồn tại trong context của trang | Đợi DOMContentLoaded hoặc networkidle rồi mới gọi |
| 5 | Dùng tên hàm đã bị minify sai | Tên trong source code không khớp thực tế | Dò tham chiếu hàm thật bằng console lúc trang đang chạy |
| 6 | Nhầm callback v2 với v2 invisible | Một số bản invisible cũng dùng callback | Kiểm tra data-size="invisible" — nếu có, xemCách giải reCAPTCHA Invisible bằng API |
Nếu form vẫn không submit sau khi đã giải token, kiểm tra theo đúng thứ tự này trước khi đào sâu hơn:
- Bạn đang gọi hàm callback hay đang set field
g-recaptcha-response? Đây là nguyên nhân số 1. - Tên hàm bạn gọi có khớp chính xác với
data-callback/grecaptcha.render()không? - Bạn gọi hàm đúng lúc trang đã sẵn sàng (
DOMContentLoaded/networkidle) chưa?
Xem đầy đủ danh sách lỗi tạiCác lỗi giải reCAPTCHA v2 thường gặp.
Ví dụ chạy được đầy đủ
Cần một project hoàn chỉnh với setup môi trường, polling, retry và xử lý lỗi sẵn?
Xem ví dụ đầy đủ trên GitHub →
Câu hỏi thường gặp
Năm câu hỏi đội automation/QA hay hỏi nhất về biến thể callback:
Callback reCAPTCHA v2 có làm khác lệnh gọi API CaptchaAI không?
Không khác chút nào. Vẫn cùng một request method=userrecaptcha với sitekey và pageurl y hệt.
Khác biệt duy nhất nằm ở bước cuối — bạn gọi hàm callback thay vì set giá trị field.
Vì sao đã nhận được token mà trang không phản ứng gì?
Nguyên nhân phổ biến nhất là bạn set g-recaptcha-response trong khi trang lại đợi callback được gọi.
Kiểm tra data-callback hoặc thuộc tính callback trong grecaptcha.render() — nếu tồn tại, bạn phải gọi đúng hàm đó, không phải set field.
reCAPTCHA v2 dạng callback có tính phí khác reCAPTCHA v2 chuẩn không?
Không — cùng một gói giá:
- CaptchaAI tính phí theo thread, không theo loại reCAPTCHA hay cách trang chèn token.
- Gói BASIC ($15/tháng, 5 thread) trở lên xử lý cả hai dạng, không phụ phí riêng cho callback.
Có bắt buộc dùng Selenium/Puppeteer để gọi callback không?
Có. Callback là một hàm JavaScript sống trong context của trang, nên bạn cần một công cụ chạy được JavaScript trên trang đích.
Selenium, Puppeteer, Playwright hoặc tương đương đều dùng được. Giải token thuần qua API (như ví dụ PHP phía trên) không đủ để tự gọi hàm — bạn vẫn cần một browser context để invoke nó.
reCAPTCHA v2 callback và reCAPTCHA v2 invisible có luôn đi cùng nhau không?
Không bắt buộc, nhưng khá phổ biến — nhiều bản invisible cũng dùng callback để nhận token. Kiểm tra data-size="invisible" trên widget; nếu có, tham khảo thêmCách giải reCAPTCHA Invisible bằng API vì luồng phát hiện hơi khác.
Bắt đầu giải reCAPTCHA v2 callback
Tóm tắt toàn bộ quy trình thành năm bước copy-paste được ngay:
- Lấy API key —captchaai.com/api.php
- Xác định tên hàm callback — kiểm tra
data-callback,grecaptcha.render()hoặc cấu hình nội bộ - Copy code Python hoặc Node.js ở trên — thay các placeholder bằng key, sitekey, pageurl và tên callback thực tế
- Chạy thử — token về sau chưa đầy 60 giây, callback được gọi, trang tự xử lý phần còn lại
- Vẫn vướng? XemCác lỗi giải reCAPTCHA v2 thường gặphoặc đọc trọnTài liệu API CaptchaAI
Bài viết liên quan
- Giải reCAPTCHA Invisible bằng Python
- Xử lý reCAPTCHA v2 và Cloudflare Turnstile trên cùng một trang
- Tìm hiểu sâu reCAPTCHA Enterprise Assessment API