Côté API, une seule chose sépare les deux variantes : le paramètre invisible=1. Côté navigateur, la facture est plus lourde : sur l'invisible, écrire le token dans le champ caché ne suffit presque jamais, il faut appeler soi-même le callback qu'un clic humain aurait déclenché. C'est l'origine numéro un du « token valide, formulaire qui ne part pas ».
- Même technologie, même format de sitekey, même champ
g-recaptcha-response. - Case à cocher : widget « Je ne suis pas un robot » visible, grille d'images possible.
- Invisible : rien à l'écran, déclenchement sur un bouton, un
submitou un chargement de page.
Ce qui change vraiment entre les deux variantes
Trois points seulement ; le reste est commun, durée de vie du token comprise.
- Le déclencheur — le widget d'un côté, un bouton ou
grecaptcha.execute()de l'autre. - Le paramètre d'envoi —
invisible=1s'ajoute à la requête. - Le callback — facultatif sur la case à cocher, quasi obligatoire sur l'invisible.
Repérer chaque variante dans le code HTML
La case à cocher v2
<!-- Standard checkbox widget -->
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-callback="onSubmit">
</div>
<!-- Widget renders as: -->
<!-- [✓] I'm not a robot reCAPTCHA logo -->
- Classe
g-recaptchasur undiv, sans attributdata-size. - C'est le clic de l'utilisateur qui enregistre le callback côté page.
- Comportement jugé suspect : un défi en grille s'ouvre sous la case.
La variante invisible
<!-- Pattern 1: Invisible widget on a button -->
<button class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-callback="onSubmit"
data-size="invisible">
Submit
</button>
<!-- Pattern 2: Invisible div (programmatic trigger) -->
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-size="invisible"
data-callback="onSubmit">
</div>
<!-- Pattern 3: Programmatic render -->
<script>
grecaptcha.render('submit-btn', {
sitekey: '6Le-wvkSAAAAAPBMRTvw...',
callback: onSubmit,
size: 'invisible'
});
</script>
Aucun widget : reCAPTCHA s'active quand l'élément désigné est déclenché ou via grecaptcha.execute(). Les trois patterns existent en production.
Détecter la variante à la volée
Ne codez jamais la variante en dur : un même site sert souvent la case sur desktop et l'invisible sur mobile.
Détection en Python
import requests
from bs4 import BeautifulSoup
import re
def detect_recaptcha_variant(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Check for invisible indicators
invisible_widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
if invisible_widget:
return {
"variant": "invisible",
"sitekey": invisible_widget.get("data-sitekey"),
"callback": invisible_widget.get("data-callback")
}
# Check for programmatic invisible in scripts
for script in soup.find_all("script"):
if script.string and "invisible" in str(script.string):
key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
if key_match:
return {
"variant": "invisible-programmatic",
"sitekey": key_match.group(1),
"callback": "check grecaptcha.render() call"
}
# Check for standard checkbox
checkbox_widget = soup.find(class_="g-recaptcha")
if checkbox_widget:
return {
"variant": "checkbox",
"sitekey": checkbox_widget.get("data-sitekey"),
"callback": checkbox_widget.get("data-callback")
}
return None
result = detect_recaptcha_variant("https://example.com/login")
print(result)
Détection en Node.js
const axios = require("axios");
const cheerio = require("cheerio");
async function detectRecaptchaVariant(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
// Check for invisible
const invisible = $(".g-recaptcha[data-size='invisible']");
if (invisible.length) {
return {
variant: "invisible",
sitekey: invisible.attr("data-sitekey"),
callback: invisible.attr("data-callback"),
};
}
// Check scripts for programmatic invisible
const scripts = $("script")
.map((_, el) => $(el).html())
.get()
.join("\n");
if (scripts.includes("invisible")) {
const keyMatch = scripts.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
if (keyMatch) {
return {
variant: "invisible-programmatic",
sitekey: keyMatch[1],
callback: "check render call",
};
}
}
// Check for standard checkbox
const checkbox = $(".g-recaptcha");
if (checkbox.length) {
return {
variant: "checkbox",
sitekey: checkbox.attr("data-sitekey"),
callback: checkbox.attr("data-callback"),
};
}
return null;
}
Vérification express dans la console du navigateur
const el = document.querySelector('[data-size="invisible"]');
console.log(el ? "Invisible reCAPTCHA" : "Checkbox reCAPTCHA");
| Signal de détection | v2 case à cocher | v2 invisible |
|---|---|---|
data-size="invisible" |
Absent | Présent |
| Case visible sur la page | Oui | Non |
Appel à grecaptcha.execute() |
Non — l'utilisateur clique | Oui — programmatique |
| Position du défi | En ligne, sous la case | Coin inférieur droit |
Tableau comparatif des deux variantes
Les temps ci-dessous sont des plafonds de service publiés, pas des moyennes : les résultats varient selon l'environnement, le volume et l'heure de la journée.
| Caractéristique | v2 case à cocher | v2 invisible |
|---|---|---|
| Widget visible | Oui — « Je ne suis pas un robot » | Non — masqué |
| Déclencheur | Clic sur la case | Bouton, submit ou chargement de page |
| Défi en images | Quand Google a un doute | Fenêtre en bas à droite |
| Friction perçue | Modérée | Faible à nulle |
| Intégration | div g-recaptcha sur la page |
data-callback ou grecaptcha.execute() |
| Format de sitekey | Identique | Identique |
| Champ de token | g-recaptcha-response |
g-recaptcha-response |
| Callback | Facultatif | Presque toujours requis |
| Méthode CaptchaAI | method=userrecaptcha |
method=userrecaptcha + invisible=1 |
| Durée de vie du token | 120 secondes | 120 secondes |
| Temps de résolution | < 60 s | < 30 s |
Envoyer la tâche à l'API CaptchaAI
La tâche part sur in.php, le résultat s'interroge sur res.php, des deux côtés.
Case à cocher v2
import requests
import time
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAA...",
"pageurl": "https://example.com/form"
})
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
Invisible v2
import requests
import time
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAA...",
"pageurl": "https://example.com/form",
"invisible": 1 # Only parameter difference
})
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
- Seule ligne ajoutée pour l'invisible :
"invisible": 1. - Clé API,
googlekey,pageurlet polling surres.php: identiques.
Injecter le token : le vrai point de divergence
Case à cocher v2 : le champ caché suffit le plus souvent
# Selenium — inject into hidden field
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# If the page uses a callback, also call it
callback = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
if callback:
driver.execute_script(f'{callback}("{token}");')
Le clic de l'utilisateur a déjà enregistré le callback : la page voit la valeur arriver.
Invisible v2 : le callback est presque toujours obligatoire
# Selenium — inject AND call the callback
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# CRITICAL: Invisible reCAPTCHA almost always requires calling the callback
callback_name = driver.find_element(
"css selector", ".g-recaptcha[data-size='invisible']"
).get_attribute("data-callback")
driver.execute_script(f'{callback_name}("{token}");')
La même séquence côté Puppeteer
// Puppeteer — invisible callback injection
await page.evaluate((tok) => {
// Set the hidden field
document.getElementById("g-recaptcha-response").value = tok;
// Find and call the callback function
const widget = document.querySelector("[data-size='invisible']");
const cbName = widget?.getAttribute("data-callback");
if (cbName && typeof window[cbName] === "function") {
window[cbName](tok);
}
}, token);
À retenir : l'invisible n'a jamais eu ce clic. Sans appel explicite du callback, la page tient la vérification pour jamais faite, token valide ou non.
- Récupérez le token sur
res.php. - Écrivez-le dans
g-recaptcha-response. - Lisez
data-callback, appelez la fonction, puis soumettez.
Un solveur unique pour les deux variantes
Une classe qui détecte puis résout évite deux chemins de code :
import requests
import time
from bs4 import BeautifulSoup
class RecaptchaV2UniversalSolver:
def __init__(self, api_key):
self.api_key = api_key
def detect_and_solve(self, page_url, page_html=None):
if not page_html:
page_html = requests.get(page_url).text
soup = BeautifulSoup(page_html, "html.parser")
# Detect variant
invisible = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
widget = invisible or soup.find(class_="g-recaptcha")
if not widget:
raise Exception("No reCAPTCHA widget found")
sitekey = widget.get("data-sitekey")
is_invisible = invisible is not None
callback = widget.get("data-callback")
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url
}
if is_invisible:
params["invisible"] = 1
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
return {
"token": result.text.split("|")[1],
"variant": "invisible" if is_invisible else "checkbox",
"callback": callback,
"sitekey": sitekey
}
if result.text != "CAPCHA_NOT_READY":
raise Exception(f"Solve error: {result.text}")
raise Exception("Timed out")
# Usage
solver = RecaptchaV2UniversalSolver("YOUR_API_KEY")
result = solver.detect_and_solve("https://example.com/login")
print(f"Variant: {result['variant']}, Callback: {result['callback']}")
Scénario : un tunnel de devis en ligne chez un assureur
Cas courant chez les équipes QA francophones : un formulaire de devis hébergé sur OVHcloud ou en région AWS eu-west-3. En recette desktop, tout passe ; en recette mobile, tout échoue avec un token pourtant valide.
Le gabarit mobile rend le widget en data-size="invisible" sur le bouton « Obtenir mon devis ». Le script écrivait le token puis cliquait, sans jamais appeler onSubmit.
- Ne testez que sur des environnements qui vous appartiennent ou que vous êtes autorisé à recetter.
- Limitez ce que vos logs de recette conservent : un devis collecte des données personnelles (obligations RGPD).
Dépannage
| Problème | Côté case à cocher | Côté invisible |
|---|---|---|
| Token refusé | Injecter dans g-recaptcha-response |
Injecter et appeler la fonction data-callback |
| Widget introuvable | Chercher .g-recaptcha |
Chercher data-size="invisible" ou grecaptcha.render() |
| Formulaire envoyé mais rejeté | Vérifier si un callback est attendu | Callback presque toujours nécessaire : le retrouver et l'appeler |
| Rechargement de page après injection | Validation JavaScript en échec | Appeler le callback avant la soumission automatique |
- Ordre de vérification : variante servie, puis callback, puis fraîcheur du token.
- Passé 120 secondes, le token est refusé même si l'injection est correcte.
Questions fréquentes
Comment savoir si une page utilise la variante invisible ?
Cherchez data-size="invisible" dans le HTML rendu, sur un div ou un bouton. Attribut absent et aucune case visible : inspectez les scripts, grecaptcha.render() peut la poser.
Pourquoi mon token est-il accepté par l'API mais refusé par le site ?
Le callback n'a pas été appelé, presque toujours. Écrire la valeur dans g-recaptcha-response ne notifie pas la page.
- La fonction nommée dans
data-callbackn'a jamais été exécutée : appelez-la avec le token. - Elle porte parfois un autre nom, posé par
grecaptcha.render().
Combien de temps un token reste-t-il valide ?
120 secondes, dans les deux cas. Résolvez au plus près de la soumission : passé ce délai, il faut relancer une résolution.
Quel plan CaptchaAI faut-il pour traiter plusieurs formulaires en parallèle ?
La facturation se fait au thread : BASIC ($15/mois, 5 threads) autorise cinq résolutions simultanées, STANDARD ($30/mois, 15 threads) quinze. C'est le nombre de tâches en vol qui compte.
Guides associés
Pour aller plus loin :
- Résoudre reCAPTCHA v2 avec l'API — côté case à cocher
- Le fonctionnement de reCAPTCHA Invisible — le mécanisme
- Résoudre reCAPTCHA Invisible avec l'API — pas à pas
- Erreurs courantes sur reCAPTCHA Invisible — correctifs
- La procédure invisible en détail
- reCAPTCHA Invisible en arrière-plan
- reCAPTCHA v2 face à reCAPTCHA v3