Quand une page charge à la fois reCAPTCHA v2 et v3, la marche à suivre tient en trois points :
- Résolvez la v3 en premier, comme le ferait le navigateur.
- Ne résolvez la v2 que si le site déclenche le repli visible.
- N'intervertissez jamais les deux sitekeys : chacune va avec sa version.
Toute la difficulté vient du « seulement si » : ce sont deux types CAPTCHA distincts, avec des sitekeys et des méthodes de résolution différentes. Le cas typique : un formulaire de connexion qui note la session avec la v3 et n'affiche la case v2 que si le score passe sous le seuil de l'éditeur.
Pourquoi ce montage combine v2 et v3
User visits page
↓
reCAPTCHA v3 runs invisibly in background
↓
Score returned to server (e.g., 0.4)
↓
Score below threshold (e.g., < 0.7)?
├─ YES → Show reCAPTCHA v2 checkbox/image challenge
└─ NO → Allow action without visible CAPTCHA
Le but est de concilier friction minimale et sécurité :
- La majorité des visiteurs (score v3 élevé) ne voient aucun défi.
- Les sessions suspectes (score v3 faible) reçoivent le défi v2.
- L'éditeur règle lui-même le seuil qui sépare l'invisible du visible.
Conséquence : la même URL peut n'exiger aucune interaction un jour et imposer un défi visible le lendemain. Votre code doit gérer les deux cas.
Schéma 1 : v3 en pré-évaluation, v2 en repli
Le montage le plus répandu. La v3 s'exécute d'abord ; la v2 n'apparaît qu'en cas de score insuffisant.
<!-- Both scripts loaded -->
<script src="https://www.google.com/recaptcha/api.js?render=V3_SITE_KEY"></script>
<script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer></script>
<form id="loginForm">
<!-- v2 widget (hidden initially) -->
<div id="recaptcha-v2-container" style="display:none;">
<div class="g-recaptcha" data-sitekey="V2_SITE_KEY"></div>
</div>
<button type="submit">Login</button>
</form>
<script>
// First attempt: v3 invisible
grecaptcha.ready(function() {
grecaptcha.execute('V3_SITE_KEY', {action: 'login'}).then(function(v3Token) {
fetch('/api/verify-v3', {
method: 'POST',
body: JSON.stringify({token: v3Token})
})
.then(r => r.json())
.then(data => {
if (data.score < 0.7) {
// Score too low → show v2 fallback
document.getElementById('recaptcha-v2-container').style.display = 'block';
grecaptcha.render('recaptcha-v2-container', {sitekey: 'V2_SITE_KEY'});
} else {
// Score OK → submit form directly
document.getElementById('loginForm').submit();
}
});
});
});
</script>
Schéma 2 : des clés différentes selon l'action
Certains sites réservent la v3 à la surveillance passive et sortent la v2 pour les actions les plus sensibles :
Homepage → v3 only (passive score)
Login page → v3 assessment, v2 fallback
Checkout → v2 always (high security)
Contact form → v3 only
Schéma 3 : un seul script, deux versions
Google permet de charger un unique script reCAPTCHA qui pilote à la fois la v2 et la v3 :
<script src="https://www.google.com/recaptcha/api.js?render=V3_SITE_KEY"></script>
<script>
// v3 execute
grecaptcha.execute('V3_SITE_KEY', {action: 'login'});
// v2 render (uses a different site key)
grecaptcha.render('v2-container', {sitekey: 'V2_SITE_KEY'});
</script>
Détecter les deux versions avant de résoudre
Confirmez d'abord que la page combine les deux versions et récupérez chaque sitekey. Deux signaux suffisent souvent :
- La v3 se lit au paramètre
renderdu script (api.js?render=...) ou à un appelgrecaptcha.execute(...). - La v2 se lit à l'attribut
data-sitekeydu widget, souvent dans une div masquée au départ.
Vérifier la page en Python
import requests
import re
def detect_dual_recaptcha(url):
"""Detect if a page uses both reCAPTCHA v2 and v3."""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
}
html = requests.get(url, headers=headers, timeout=15).text
result = {
"has_v3": False,
"has_v2": False,
"v3_site_key": None,
"v2_site_key": None,
"dual": False,
"pattern": None,
}
# Detect v3 (render parameter or enterprise.execute)
v3_match = re.search(r"api\.js\?render=([A-Za-z0-9_-]+)", html)
if v3_match and v3_match.group(1) != "explicit":
result["has_v3"] = True
result["v3_site_key"] = v3_match.group(1)
# Detect v3 in execute calls
v3_execute = re.search(
r"grecaptcha\.(?:enterprise\.)?execute\s*\(\s*['\"]([^'\"]+)['\"]",
html,
)
if v3_execute:
result["has_v3"] = True
if not result["v3_site_key"]:
result["v3_site_key"] = v3_execute.group(1)
# Detect v2 (g-recaptcha class or explicit render)
v2_match = re.search(r'data-sitekey="([^"]+)"', html)
if v2_match:
key = v2_match.group(1)
if key != result.get("v3_site_key"):
result["has_v2"] = True
result["v2_site_key"] = key
# Check for explicit v2 render
v2_render = re.search(
r"grecaptcha\.render\s*\([^,]+,\s*\{[^}]*sitekey:\s*['\"]([^'\"]+)",
html,
)
if v2_render:
result["has_v2"] = True
if not result["v2_site_key"]:
result["v2_site_key"] = v2_render.group(1)
result["dual"] = result["has_v3"] and result["has_v2"]
if result["dual"]:
# Determine pattern
if "display:none" in html or "display: none" in html:
result["pattern"] = "v3_pre_assessment_v2_fallback"
else:
result["pattern"] = "v2_v3_simultaneous"
return result
detection = detect_dual_recaptcha("https://example.com/login")
print(detection)
Vérifier la page en Node.js
const axios = require("axios");
async function detectDualRecaptcha(url) {
const { data: html } = await axios.get(url, { timeout: 15000 });
const result = {
hasV3: false,
hasV2: false,
v3SiteKey: null,
v2SiteKey: null,
dual: false,
};
// v3 detection
const v3Match = html.match(/api\.js\?render=([A-Za-z0-9_-]+)/);
if (v3Match && v3Match[1] !== "explicit") {
result.hasV3 = true;
result.v3SiteKey = v3Match[1];
}
// v2 detection
const v2Match = html.match(/data-sitekey="([^"]+)"/);
if (v2Match && v2Match[1] !== result.v3SiteKey) {
result.hasV2 = true;
result.v2SiteKey = v2Match[1];
}
result.dual = result.hasV3 && result.hasV2;
return result;
}
detectDualRecaptcha("https://example.com/login").then(console.log);
Résoudre le double reCAPTCHA avec CaptchaAI
Avec les deux sitekeys en main, trois approches selon ce que vous savez du site :
- Vous ignorez si la v2 s'affichera → approche 1 (v3 puis v2).
- La v2 s'affiche toujours pour votre trafic → approche 2 (v2 directe).
- Montage complexe ou dynamique → approche 3 (navigateur).
Côté coût, CaptchaAI facture au thread (une résolution en cours), pas à la résolution : chaque plan inclut des résolutions illimitées par thread. Le plan BASIC ($15/mois, 5 threads) couvre donc autant de résolutions v2 et v3 que nécessaire. Ce qui compte, c'est le nombre mené en parallèle.
Approche 1 : la v3 d'abord, la v2 au besoin
Cette stratégie reproduit le flux du site lui-même :
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_v3(site_key, page_url, action="login"):
"""Solve reCAPTCHA v3 and return token."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"version": "v3",
"action": action,
"json": 1,
}).json()
task_id = submit["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("v3 solve timeout")
def solve_v2(site_key, page_url):
"""Solve reCAPTCHA v2 and return token."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1,
}).json()
task_id = submit["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("v2 solve timeout")
def solve_dual_recaptcha(v3_key, v2_key, page_url, action="login"):
"""Handle dual reCAPTCHA: try v3, fall back to v2."""
# Step 1: Try v3
v3_token = solve_v3(v3_key, page_url, action)
# Step 2: Submit v3 token to target
response = requests.post(f"{page_url}/verify", data={
"g-recaptcha-response": v3_token,
})
# Step 3: Check if v2 fallback is needed
if "recaptcha" in response.text.lower() and v2_key:
print("v3 score too low — v2 fallback triggered")
v2_token = solve_v2(v2_key, page_url)
return {"version": "v2", "token": v2_token}
return {"version": "v3", "token": v3_token}
result = solve_dual_recaptcha(
v3_key="6LcExample_v3_key",
v2_key="6LcExample_v2_key",
page_url="https://example.com/login",
)
print(f"Solved with {result['version']}")
Approche 2 : viser directement la v2
Si le site sert toujours la v2 au trafic automatisé (le score v3 restera bas), inutile de payer une résolution v3 vouée à échouer : attaquez directement la v2.
# If you consistently fail v3 assessment, just solve v2 directly
token = solve_v2(v2_site_key, page_url)
submit_form(token)
Vous économisez le temps et le thread mobilisés par une v3 qui ne franchira pas le seuil.
Approche 3 : piloter le repli au navigateur
Pour les montages complexes, laissez un navigateur gérer le basculement et lire l'état réel du widget v2 :
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get("https://example.com/login")
time.sleep(3)
# Check if v2 widget is visible
v2_visible = driver.execute_script("""
const container = document.querySelector('.g-recaptcha');
if (!container) return false;
const style = window.getComputedStyle(container.parentElement);
return style.display !== 'none' && style.visibility !== 'hidden';
""")
if v2_visible:
# v2 is showing — solve v2
sitekey = driver.find_element(
By.CSS_SELECTOR, "[data-sitekey]"
).get_attribute("data-sitekey")
token = solve_v2(sitekey, driver.current_url)
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
else:
# v3 only — solve v3
# Extract v3 key from page source
v3_key = driver.execute_script(
"return document.querySelector('script[src*=\"render=\"]')"
".src.match(/render=([^&]+)/)[1];"
)
token = solve_v3(v3_key, driver.current_url)
# Inject v3 token into the form
driver.execute_script(f"""
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'g-recaptcha-response';
input.value = '{token}';
document.querySelector('form').appendChild(input);
""")
driver.find_element(By.CSS_SELECTOR, "form").submit()
Gérer deux sitekeys sur la même page
Un site en double reCAPTCHA expose deux sitekeys différentes ; les confondre produit un token invalide, rejeté à la soumission. Retenez leur emplacement :
- Clé v3 : dans l'URL du script
?render=KEYet dansgrecaptcha.execute('KEY', ...). - Clé v2 : dans le
data-sitekey="KEY"de la div du widget.
reCAPTCHA Enterprise avec repli v2
Certaines intégrations Enterprise notent la session avec la v3 Enterprise et gardent la v2 classique pour le défi visible :
# Detect and handle Enterprise + v2 combo
if "recaptcha/enterprise.js" in html:
# Use enterprise parameter for v3
v3_params = {"enterprise": 1, "version": "v3"}
else:
v3_params = {"version": "v3"}
Plusieurs formulaires sur la même page
Si une page comporte plusieurs formulaires (connexion + inscription), chacun peut embarquer sa propre instance reCAPTCHA. Extrayez la sitekey du formulaire précis que vous ciblez :
# Target the login form specifically
login_form = soup.find("form", id="login-form")
widget = login_form.find(attrs={"data-sitekey": True})
sitekey = widget["data-sitekey"]
Dépannage rapide
Les ratés viennent presque toujours d'une clé mal attribuée ou d'un repli mal lu :
| Symptôme | Cause probable | Correctif |
|---|---|---|
Le site rejette le token (invalid-input-response) |
Mauvaise sitekey pour la version résolue | Résolvez la v3 avec la clé ?render=, la v2 avec data-sitekey |
| La v2 apparaît alors que la v3 a renvoyé un token | Score v3 sous le seuil : le repli s'est déclenché | Enchaînez une résolution v2, c'est le comportement attendu |
| Le conteneur v2 reste masqué après la soumission | Le repli est décidé côté serveur, pas dans le DOM | Analysez la réponse HTTP renvoyée après l'envoi du token v3 |
| Résolution v3 en timeout systématique | version ou action absent de la requête |
Transmettez version: v3 et l'action attendue (ex. login) |
Questions fréquentes
Un double reCAPTCHA consomme-t-il deux threads sur mon plan ?
Non, pas en parallèle dans le cas courant. Vous résolvez la v3, puis la v2 seulement si le repli se déclenche : les résolutions s'enchaînent au lieu d'occuper deux threads en même temps. Comme chaque plan inclut un nombre de résolutions illimité par thread, ce parcours n'entraîne aucun surcoût unitaire.
Comment détecter la double implémentation sans navigateur ?
Une requête HTTP suffit le plus souvent : récupérez le HTML, cherchez le paramètre ?render= du script (v3) et l'attribut data-sitekey du widget (v2). Réservez Selenium aux pages qui injectent le widget v2 par JavaScript après le chargement.
Que faire si le site utilise reCAPTCHA Enterprise pour la v3 ?
Traitez la v3 comme une résolution Enterprise (paramètre enterprise) et gardez la v2 classique. Le repli est identique : seule la notation du score change.
En résumé
Une page en double reCAPTCHA note la session avec la v3 et n'affiche la v2 que si le score tombe sous le seuil. Repérez chaque version via le paramètre render (v3) et le data-sitekey (v2), puis appliquez la règle : résolvez la v3 avec CaptchaAI, soumettez le token, et ne passez à la v2 qu'en cas de repli. Chaque version est un appel API distinct, avec sa propre sitekey.
Articles connexes
- reCAPTCHA dans une application monopage (SPA)
- Déclencher le callback reCAPTCHA v2 via l'API
- reCAPTCHA v2 et Turnstile sur le même site