Comparisons

reCAPTCHA v2 vs Invisible reCAPTCHA expliqué

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 submit ou 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'envoiinvisible=1 s'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-recaptcha sur un div, sans attribut data-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, pageurl et polling sur res.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.

  1. Récupérez le token sur res.php.
  2. Écrivez-le dans g-recaptcha-response.
  3. 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-callback n'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 :

Les commentaires sont désactivés pour cet article.