Токен от CaptchaAI пришёл, а форма всё равно не отправилась? В invisible reCAPTCHA почти всегда виновата не сама капча, а один из типовых сбоев: виджет не найден на странице, sitekey перепутан, забыт параметр invisible=1, токен истёк или после решения не вызван callback.
Ниже — разбор каждой причины с точным исправлением и рабочим кодом на Python и Node.js. Если нужен обзор того, как вообще устроен invisible-режим, сначала загляните в Как работает reCAPTCHA Invisible и как его решить.
Пять причин, из-за которых invisible reCAPTCHA чаще всего не срабатывает в автоматизации:
- виджет вообще не найден на странице — парсер ищет чекбокс, которого у invisible-режима нет;
- sitekey скопирован не с того виджета или не с той страницы;
- токен внедрён, но JS-
callbackне вызван — форма молчит; - в запросе к
in.phpзабыт параметрinvisible=1; - токен использован спустя больше 120 секунд после решения.
Шпаргалка: симптом → причина → исправление
| Симптом / ошибка | Причина | Исправление |
|---|---|---|
| Виджет не найден в HTML | У invisible reCAPTCHA нет чекбокса — обычный селектор его пропускает | Ищите data-size="invisible" или size: 'invisible' в grecaptcha.render() |
ERROR_WRONG_GOOGLEKEY |
sitekey скопирован с другого виджета или другой страницы | Извлекайте sitekey конкретно из invisible-виджета |
| Токен принят, но форма не отправляется | Не вызван data-callback после внедрения токена |
Найдите callback-функцию и вызовите её с токеном |
Забыт invisible=1 |
Задача решена как обычный v2-чекбокс | Всегда добавляйте invisible=1 в запрос к in.php |
CAPCHA_NOT_READY дольше обычного |
Задача ещё обрабатывается | Опрашивайте res.php каждые 5 секунд, решение занимает 10–30 секунд |
ERROR_BAD_TOKEN_OR_PAGEURL |
pageurl не совпадает, токен переиспользован или IP другой |
Сверьте pageurl, решайте заново на каждую отправку, добавьте proxy |
| На странице несколько виджетов | Форма держит одновременно v2-чекбокс и invisible-виджет | Берите именно виджет с data-size="invisible" |
ERROR_KEY_DOES_NOT_EXIST |
Неверный API-ключ CaptchaAI | Проверьте ключ в captchaai.com/account |
Виджет не обнаружен: парсер не видит invisible reCAPTCHA
У invisible reCAPTCHA нет визуального чекбокса, поэтому классическая проверка «есть ли галочка на странице» ничего не даёт. Если ваш парсер не распознаёт виджет, запросы, защищённые капчей, проваливаются молча — ошибкой отправки формы или редиректом без объяснений.
Как распознать invisible reCAPTCHA в HTML
Ищите на странице один из трёх шаблонов:
<!-- Pattern 1: div with data-size="invisible" -->
<div class="g-recaptcha" data-sitekey="6LdKlZEU..."
data-size="invisible"
data-callback="onSubmit"></div>
<!-- Pattern 2: button with data-sitekey and invisible size -->
<button class="g-recaptcha"
data-sitekey="6LdKlZEU..."
data-callback="onSubmit"
data-action="submit">Submit</button>
<!-- Pattern 3: programmatic render with size: invisible -->
<script>
grecaptcha.render('submit-btn', {
sitekey: '6LdKlZEU...',
callback: onSubmit,
size: 'invisible'
});
</script>
Скрипт обнаружения на Python:
import requests
from bs4 import BeautifulSoup
import re
def detect_invisible_recaptcha(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Check for data-size="invisible"
widget = soup.find("div", {"data-size": "invisible", "class": "g-recaptcha"})
if widget:
return {
"type": "invisible",
"sitekey": widget.get("data-sitekey"),
"callback": widget.get("data-callback")
}
# Check for programmatic render with invisible
scripts = soup.find_all("script")
for script in scripts:
if script.string and "size" in str(script.string) and "invisible" in str(script.string):
key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
if key_match:
return {
"type": "invisible-programmatic",
"sitekey": key_match.group(1),
"callback": "check grecaptcha.render() call"
}
return None
Тот же скрипт на Node.js:
const axios = require("axios");
const cheerio = require("cheerio");
async function detectInvisibleRecaptcha(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
// Check for data-size="invisible"
const widget = $(".g-recaptcha[data-size='invisible']");
if (widget.length) {
return {
type: "invisible",
sitekey: widget.attr("data-sitekey"),
callback: widget.attr("data-callback"),
};
}
// Check script tags for programmatic invisible render
const scriptContent = $("script")
.map((_, el) => $(el).html())
.get()
.join("\n");
if (scriptContent.includes("invisible")) {
const keyMatch = scriptContent.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
if (keyMatch) {
return {
type: "invisible-programmatic",
sitekey: keyMatch[1],
callback: "check grecaptcha.render() call",
};
}
}
return null;
}
Ошибка ERROR_WRONG_GOOGLEKEY: перепутан sitekey
Эта ошибка означает, что отправленный sitekey не соответствует invisible-виджету на целевой странице.
Типичные причины:
- sitekey скопирован с v2-чекбокса на другой странице сайта;
- использован ключ из anchor-URL другой версии reCAPTCHA;
- на странице несколько виджетов reCAPTCHA (разбор такого случая — ниже), и вы взяли не тот.
Если sitekey недавно менялся, сверяйте значение прямо в DevTools на целевой странице, а не с сохранённой копии в коде.
Как извлечь правильный invisible sitekey
import requests
from bs4 import BeautifulSoup
def get_invisible_sitekey(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Priority 1: invisible widget
widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
if widget:
return widget["data-sitekey"]
# Priority 2: any g-recaptcha div (may be invisible without data-size)
widget = soup.find(class_="g-recaptcha")
if widget and widget.get("data-sitekey"):
return widget["data-sitekey"]
return None
sitekey = get_invisible_sitekey("https://staging.example.com/qa-login")
print(f"Sitekey: {sitekey}")
Самая частая проблема: не вызван callback
Это ошибка номер один, которую упускают при автоматизации invisible reCAPTCHA. В v2-чекбоксе достаточно подставить токен в поле
g-recaptcha-response— и всё сработает. С invisible почти всегда нужен ещё один шаг: вызвать JavaScript-функцию обратного вызова. Если внедрить токен, но не вызвать callback, форма его просто проигнорирует.
Как устроен поток callback
grecaptcha.execute()запускает invisible-проверку.- После решения Google вызывает функцию, указанную в
data-callback. - Эта функция и отправляет форму или делает вызов API — сама по себе.
Быстрая проверка: вызовите найденную функцию в консоли DevTools вручную (
onSubmit("test")). Не инициирует отправку — вы нашли не тот callback.
Как найти и вызвать callback
Шаг 1 — определите имя callback-функции:
# From HTML: data-callback="onSubmit"
# From JS: callback: onSubmit
# From grecaptcha.render: second argument with callback property
Шаг 2 — внедрите токен И вызовите callback (Selenium):
from selenium import webdriver
import requests
import time
driver = webdriver.Chrome()
driver.get("https://example.com/form")
# Get sitekey
sitekey = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-sitekey")
callback_name = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
# Solve with CaptchaAI
task_id = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": driver.current_url,
"invisible": 1
}).text.split("|")[1]
# Poll for result
token = None
for _ in range(60):
time.sleep(5)
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id
}).text
if resp.startswith("OK|"):
token = resp.split("|")[1]
break
# Inject token into the response field
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# CRITICAL: Call the callback function
driver.execute_script(f'{callback_name}("{token}");')
Тот же шаг на Puppeteer:
const puppeteer = require("puppeteer");
const axios = require("axios");
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.goto("https://example.com/form");
// Get sitekey and callback
const { sitekey, callback } = await page.evaluate(() => {
const el = document.querySelector(".g-recaptcha[data-size='invisible']");
return {
sitekey: el?.getAttribute("data-sitekey"),
callback: el?.getAttribute("data-callback"),
};
});
// Submit to CaptchaAI
const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
params: {
key: "YOUR_API_KEY",
method: "userrecaptcha",
googlekey: sitekey,
pageurl: page.url(),
invisible: 1,
},
});
const taskId = submitResp.data.split("|")[1];
// Poll for result
let token;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: "YOUR_API_KEY", action: "get", id: taskId },
});
if (result.data.startsWith("OK|")) {
token = result.data.split("|")[1];
break;
}
}
// Inject token and fire callback
await page.evaluate(
(tok, cb) => {
document.getElementById("g-recaptcha-response").value = tok;
if (cb && typeof window[cb] === "function") {
window[cb](tok);
}
},
token,
callback,
);
await browser.close();
})();
Забыт параметр invisible=1
При решении invisible reCAPTCHA через CaptchaAI параметр
invisible=1обязателен. Без него solver обрабатывает задачу как стандартный v2-чекбокс — итог непредсказуем: либоERROR_CAPTCHA_UNSOLVABLE, либо токен, который целевой сайт молча отклонит.
Неправильный и правильный запрос
# WRONG — missing invisible=1
params = {
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url
}
# CORRECT — includes invisible=1
params = {
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1 # Required for invisible reCAPTCHA
}
response = requests.get("https://ocr.captchaai.com/in.php", params=params)
Токен истёк до отправки
Токен invisible reCAPTCHA живёт 120 секунд — столько же, сколько у стандартного v2. Но у invisible-сценариев обычно больше промежуточных шагов между решением и отправкой формы, поэтому токен чаще успевает протухнуть до того, как доедет до сервера.
Симптомы
| Симптом | Что это значит |
|---|---|
| Форма возвращает общую ошибку сразу после внедрения токена | Токен, скорее всего, уже протух к моменту отправки |
Серверная проверка siteverify отвечает timeout-or-duplicate |
Токен пришёл на сервер позже 120-секундного окна |
| Токен был валиден, но до шага отправки дошёл слишком поздно | Между решением и отправкой формы слишком много промежуточных шагов |
Решение: решайте капчу непосредственно перед отправкой
Запрашивайте решение только тогда, когда готовы отправить форму немедленно:
import requests
import time
def solve_invisible_recaptcha(api_key, sitekey, page_url):
# Submit task
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
})
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
# Poll for result
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
})
if result.text.startswith("OK|"):
return result.text.split("|")[1]
if result.text != "CAPCHA_NOT_READY":
raise Exception(f"Solve failed: {result.text}")
raise Exception("Solve timed out after 5 minutes")
# Usage: solve JUST before you need to submit
# 1. Navigate to page and prepare form data first
# 2. THEN solve the captcha
# 3. Inject token and submit immediately
token = solve_invisible_recaptcha("YOUR_API_KEY", sitekey, page_url)
# Submit within 120 seconds of receiving the token
ERROR_BAD_TOKEN_OR_PAGEURL: сайт отклонил токен
Целевой сайт проверил токен у Google и получил отказ.
Типичные причины:
| Причина | Как проверить | Исправление |
|---|---|---|
Неверный pageurl |
URL не совпадает с доменом, на который зарегистрирован sitekey | Используйте точный URL, на котором рендерится виджет |
| Токен решён для другого домена | Кросс-доменное переиспользование токена | Решайте капчу с pageurl нужного домена |
| Токен уже использован | Одна и та же отправка токена дважды | Запрашивайте новое решение на каждую попытку отправки |
| IP не совпадает | IP solver'а отличается от IP сессии | Добавьте параметр proxy, соответствующий IP сессии |
| Не передан флаг invisible | Решено как v2, использовано на invisible-странице | Добавьте invisible=1 в запрос на решение |
Частый сценарий для этой аудитории: форму тестируют локально в Москве, Алматы или Минске, а продакшен работает на европейском или казахстанском хостинге. IP solver'а и IP продакшен-сервера расходятся, и сайт отдаёт
ERROR_BAD_TOKEN_OR_PAGEURLтолько в проде, хотя локально всё было нормально. Обычно достаточно добавить параметрproxy, указывающий на регион продакшена.
Чек-лист отладки
def debug_invisible_solve(api_key, sitekey, page_url, proxy=None):
"""Run a diagnostic solve with detailed logging."""
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")
print(f"Proxy: {proxy or 'none'}")
params = {
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
}
if proxy:
params["proxy"] = proxy
params["proxytype"] = "HTTP"
# Submit
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
print(f"Submit response: {resp.text}")
if not resp.text.startswith("OK|"):
return None
task_id = resp.text.split("|")[1]
print(f"Task ID: {task_id}")
# Poll with timing
start = time.time()
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
})
elapsed = time.time() - start
print(f" [{elapsed:.0f}s] {result.text[:50]}")
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
print(f"Token received after {elapsed:.0f}s")
print(f"Token length: {len(token)} characters")
print(f"Token starts with: {token[:30]}...")
return token
if result.text != "CAPCHA_NOT_READY":
print(f"FAILED: {result.text}")
return None
print("TIMEOUT after 5 minutes")
return None
Несколько виджетов reCAPTCHA на одной странице
Часть форм держит сразу два виджета: видимый v2-чекбокс (например, на логине) и invisible-виджет (на форме отправки). Если решить не тот виджет, токен будет валидным, но не подойдёт к действию, которое вы автоматизируете — и ошибка будет выглядеть загадочно, потому что API вернёт нормальный
OK|токен.
Как выбрать нужный виджет
from bs4 import BeautifulSoup
def find_all_recaptcha_widgets(html):
soup = BeautifulSoup(html, "html.parser")
widgets = []
for el in soup.find_all(class_="g-recaptcha"):
widgets.append({
"sitekey": el.get("data-sitekey"),
"size": el.get("data-size", "normal"),
"callback": el.get("data-callback"),
"tag": el.name,
"id": el.get("id")
})
return widgets
# Example output:
# [
# {"sitekey": "6LdA...", "size": "normal", "callback": None, "tag": "div", "id": "recaptcha-login"},
# {"sitekey": "6LdB...", "size": "invisible", "callback": "onRegister", "tag": "div", "id": "recaptcha-register"}
# ]
# Use the widget with size="invisible" for the invisible solve
Берите виджет с size: "invisible" для invisible-решения, а не первый попавшийся g-recaptcha на странице.
Готовый solver для invisible reCAPTCHA с обработкой ошибок
Ниже — обёртка, которая сразу закрывает все причины сбоев выше: обязательный
invisible=1, автоматический повтор приERROR_CAPTCHA_UNSOLVABLE, и чёткое разделение конфигурационных ошибок (не имеет смысла повторять) от временных (имеет смысл).
import requests
import time
import logging
logger = logging.getLogger(__name__)
class InvisibleRecaptchaSolver:
def __init__(self, api_key, max_retries=3):
self.api_key = api_key
self.max_retries = max_retries
self.base_url = "https://ocr.captchaai.com"
def solve(self, sitekey, page_url, proxy=None):
"""Solve invisible reCAPTCHA with automatic retry on transient errors."""
for attempt in range(1, self.max_retries + 1):
try:
token = self._attempt_solve(sitekey, page_url, proxy)
if token:
return token
except Exception as e:
logger.warning(f"Attempt {attempt} failed: {e}")
if attempt < self.max_retries:
time.sleep(2 ** attempt)
raise Exception(f"Failed to solve after {self.max_retries} attempts")
def _attempt_solve(self, sitekey, page_url, proxy):
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
}
if proxy:
params["proxy"] = proxy
params["proxytype"] = "HTTP"
# Submit task
resp = requests.get(f"{self.base_url}/in.php", params=params)
if "ERROR" in resp.text:
error = resp.text.strip()
if error in ("ERROR_WRONG_GOOGLEKEY", "ERROR_KEY_DOES_NOT_EXIST"):
raise Exception(f"Configuration error (do not retry): {error}")
if error == "ERROR_ZERO_BALANCE":
raise Exception("Account balance is zero — add funds")
raise Exception(f"Submit error: {error}")
if not resp.text.startswith("OK|"):
raise Exception(f"Unexpected submit response: {resp.text}")
task_id = resp.text.split("|")[1]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get(f"{self.base_url}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id
})
if result.text.startswith("OK|"):
return result.text.split("|")[1]
if result.text == "CAPCHA_NOT_READY":
continue
if result.text == "ERROR_CAPTCHA_UNSOLVABLE":
logger.warning("Captcha unsolvable — will retry with new task")
return None
raise Exception(f"Poll error: {result.text}")
raise Exception("Solve timed out after 5 minutes")
# Usage
solver = InvisibleRecaptchaSolver("YOUR_API_KEY")
token = solver.solve(
sitekey="6LdKlZEU...",
page_url="https://staging.example.com/qa-login"
)
print(f"Token: {token[:50]}...")
const axios = require("axios");
class InvisibleRecaptchaSolver {
constructor(apiKey, maxRetries = 3) {
this.apiKey = apiKey;
this.maxRetries = maxRetries;
this.baseUrl = "https://ocr.captchaai.com";
}
async solve(sitekey, pageUrl, proxy) {
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const token = await this._attemptSolve(sitekey, pageUrl, proxy);
if (token) return token;
} catch (err) {
console.warn(`Attempt ${attempt} failed: ${err.message}`);
if (attempt < this.maxRetries) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
}
throw new Error(`Failed to solve after ${this.maxRetries} attempts`);
}
async _attemptSolve(sitekey, pageUrl, proxy) {
const params = {
key: this.apiKey,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageUrl,
invisible: 1,
};
if (proxy) {
params.proxy = proxy;
params.proxytype = "HTTP";
}
// Submit task
const submitResp = await axios.get(`${this.baseUrl}/in.php`, { params });
if (submitResp.data.includes("ERROR")) {
const error = submitResp.data.trim();
if (["ERROR_WRONG_GOOGLEKEY", "ERROR_KEY_DOES_NOT_EXIST"].includes(error)) {
throw new Error(`Configuration error (do not retry): ${error}`);
}
throw new Error(`Submit error: ${error}`);
}
const taskId = submitResp.data.split("|")[1];
// Poll for result
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get(`${this.baseUrl}/res.php`, {
params: { key: this.apiKey, action: "get", id: taskId },
});
if (result.data.startsWith("OK|")) {
return result.data.split("|")[1];
}
if (result.data === "CAPCHA_NOT_READY") continue;
if (result.data === "ERROR_CAPTCHA_UNSOLVABLE") return null;
throw new Error(`Poll error: ${result.data}`);
}
throw new Error("Solve timed out after 5 minutes");
}
}
// Usage
const solver = new InvisibleRecaptchaSolver("YOUR_API_KEY");
solver.solve("6LdKlZEU...", "https://staging.example.com/qa-login").then((token) => {
console.log(`Token: ${token.substring(0, 50)}...`);
});
Чек-лист диагностики
Если invisible reCAPTCHA не решается, пройдите по пунктам по порядку — в подавляющем большинстве случаев причина находится на первых четырёх шагах.
| Шаг | Что проверить | Как |
|---|---|---|
| 1 | Это действительно invisible, а не v2-чекбокс | Ищите data-size="invisible" или size: 'invisible' в коде рендера |
| 2 | sitekey верный | Сравните с data-sitekey именно у invisible-виджета |
| 3 | invisible=1 есть в запросе |
Проверьте параметры in.php |
| 4 | pageurl совпадает точно |
Берите URL из DevTools браузера, а не из редиректа |
| 5 | Найдено имя callback-функции | Ищите data-callback или callback в grecaptcha.render() |
| 6 | Токен внедрён И callback вызван | Оба шага обязательны — одного токена мало |
| 7 | Токен свежий | Используйте его в течение 120 секунд |
| 8 | Проверено с прокси | Добавьте proxy и proxytype, если подозреваете расхождение IP |
Часто задаваемые вопросы
Как отличить invisible reCAPTCHA от обычного чекбокса v2 на странице?
Смотрите на атрибуты виджета: data-size="invisible" в HTML или size: 'invisible' в вызове grecaptcha.render(). Если ни того, ни другого нет, скорее всего это стандартный v2-чекбокс, и внедрять callback не нужно.
Почему форма не отправляется, хотя CaptchaAI вернул токен?
Почти всегда причина в пропущенном callback. Подставить токен в g-recaptcha-response недостаточно — invisible reCAPTCHA обычно ждёт вызова JavaScript-функции из data-callback, и только она инициирует реальную отправку формы или вызов API.
Сколько времени в среднем занимает решение invisible reCAPTCHA?
Через CaptchaAI типичное время — 10–30 секунд. Invisible-задачи обычно решаются быстрее, чем v2-чекбокс, потому что не требуют распознавания изображений — решение строится на анализе риска.
Почему pageurl и IP имеют значение, если решение полностью серверное?
Google проверяет не только сам токен, но и то, откуда пришёл соответствующий запрос: домен из pageurl и IP, с которого решалась задача. Если продакшен-сервер и solver выходят в сеть с разных IP, добавьте параметр proxy, совпадающий с IP вашей рабочей сессии.
Можно ли использовать один и тот же токен дважды для повторной отправки формы?
Нет. Токен одноразовый — как только сайт его проверил, повторная отправка того же токена вернёт ERROR_BAD_TOKEN_OR_PAGEURL или аналогичный отказ. На каждую попытку отправки формы запрашивайте новое решение.
Когда стоит писать в поддержку CaptchaAI
Если чек-лист пройден полностью — invisible=1 передаётся, sitekey верный, callback вызывается, токен свежий — а сайт всё равно отклоняет решение, дальнейшая самодиагностика обычно неэффективна.
Приложите к тикету task_id неудачной задачи, pageurl и время попытки — это ускоряет разбор.