Explainers

Le callback reCAPTCHA v2 : le déclencher après l'injection du token

Le token est bien écrit dans le textarea g-recaptcha-response, et pourtant le bouton d'envoi reste grisé. Dans la grande majorité des cas, il ne manque qu'une chose : l'appel de la fonction de callback que le site a enregistrée auprès du widget reCAPTCHA v2.

Injecter un token résolu par CaptchaAI ne fait que remplir un champ caché. C'est le callback — déclaré par l'attribut data-callback ou par l'option callback de grecaptcha.render() — qui active le bouton, valide le formulaire ou envoie la requête AJAX attendue. Tant qu'il n'est pas appelé, la page se comporte exactement comme si le défi CAPTCHA n'avait jamais été résolu.

Voici comment retrouver ce callback dans le DOM, l'appeler depuis Selenium ou Puppeteer, et quoi faire quand le site n'en expose aucun.


Ce que le callback reCAPTCHA v2 déclenche vraiment

Le site déclare le callback directement sur le widget :

<div class="g-recaptcha"
     data-sitekey="6Le-SITEKEY"
     data-callback="onCaptchaSuccess"
     data-expired-callback="onCaptchaExpired">
</div>

<script>
function onCaptchaSuccess(token) {
  document.getElementById('submit-btn').disabled = false;
  document.getElementById('captcha-token').value = token;
}
</script>

Quand un visiteur valide la case, le JavaScript de Google appelle onCaptchaSuccess(token) et lui transmet la chaîne du token comme unique argument. Le widget fait donc deux choses distinctes : il écrit le token dans le textarea, puis il notifie la page.

Une injection faite depuis votre script ne reproduit que la première. Retenez cette séparation : elle explique presque tous les formulaires bloqués après une résolution réussie. Côté API tout va bien, côté navigateur personne n'a prévenu la page.


Retrouver le nom de la fonction de callback

Le nom est presque toujours lisible dans la page, mais pas au même endroit selon les sites. Testez ces trois méthodes dans l'ordre : la première couvre les formulaires classiques, la troisième les widgets rendus par un framework.

Méthode 1 : lire l'attribut data-callback

// In browser console
const widget = document.querySelector('.g-recaptcha');
const callbackName = widget?.getAttribute('data-callback');
console.log('Callback:', callbackName);

Si la console renvoie un nom, vous tenez une fonction globale appelable via window[nom].

Méthode 2 : inspecter l'appel à grecaptcha.render()

Certains sites passent le callback en option de grecaptcha.render() plutôt que par l'attribut data-callback :

// Search page source for grecaptcha.render
document.querySelectorAll('script:not([src])').forEach(s => {
  if (s.textContent.includes('grecaptcha.render')) {
    console.log(s.textContent.match(/callback\s*:\s*(\w+)/)?.[1]);
  }
});

Méthode 3 : intercepter l'enregistrement du callback

À exécuter dans les DevTools avant le chargement de la page (Sources → Snippets) :

const origRender = grecaptcha.render;
grecaptcha.render = function(container, params) {
  console.log('Render callback:', params.callback);
  console.log('Expired callback:', params['expired-callback']);
  return origRender.apply(this, arguments);
};

Réservez-la aux applications React ou Vue, où le widget est monté après coup et où aucun nom de fonction n'apparaît dans le HTML servi.


Injecter le token, puis appeler le callback

Python (Selenium)

import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example.com/login")

# Extract sitekey and callback
sitekey = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")

callback = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-callback")

# Solve with CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": driver.current_url,
    "json": "1",
}).json()
task_id = resp["request"]

token = None
for _ in range(24):
    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["status"] == 1:
        token = result["request"]
        break

# Inject token into textarea
driver.execute_script("""
    document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
""", token)

# Trigger the callback
if callback:
    driver.execute_script(f"window['{callback}'](arguments[0]);", token)
    print(f"Triggered callback: {callback}")
else:
    # Fallback: try ___grecaptcha_cfg
    driver.execute_script("""
        try {
            var widgetId = Object.keys(___grecaptcha_cfg.clients)[0];
            var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
            if (typeof callback === 'function') callback(arguments[0]);
        } catch(e) {}
    """, token)
    print("Triggered callback via ___grecaptcha_cfg")

Deux détails comptent ici. Le polling interroge res.php toutes les 5 secondes, 24 fois au maximum : la boucle laisse donc deux minutes avant d'abandonner. Et la solution de repli ___grecaptcha_cfg sert quand le callback est enfermé dans une closure : le chemin aa.l.callback dépend d'une version minifiée du script de Google, gardez-le dans un try.

Node.js (Puppeteer)

const puppeteer = require('puppeteer');

// After solving and getting the token...
await page.evaluate((token, callbackName) => {
  // Set textarea value
  const textarea = document.querySelector(
    'textarea[name="g-recaptcha-response"]'
  );
  textarea.value = token;
  textarea.style.display = 'block'; // sometimes hidden

  // Trigger callback
  if (callbackName && typeof window[callbackName] === 'function') {
    window[callbackName](token);
    console.log(`Called ${callbackName}()`);
  } else {
    // Fallback: search grecaptcha config
    try {
      const clients = ___grecaptcha_cfg.clients;
      const widgetId = Object.keys(clients)[0];
      const cb = clients[widgetId]?.aa?.l?.callback;
      if (typeof cb === 'function') cb(token);
    } catch (e) {}
  }
}, token, callbackName);

Le passage à display: block sur le textarea n'est pas cosmétique : certaines validations côté client ignorent les champs masqués et le formulaire refuse alors une valeur pourtant correcte.


Quand le site n'expose aucun data-callback

Une partie des formulaires n'enregistre rien du tout et se contente d'interroger grecaptcha.getResponse() au moment de l'envoi. Il n'y a alors aucune fonction à appeler : c'est la lecture de la réponse qu'il faut satisfaire.

driver.execute_script("""
    const token = arguments[0];
    document.querySelector('textarea[name="g-recaptcha-response"]').value = token;
    // Override getResponse to return the token
    if (typeof grecaptcha !== 'undefined') {
        grecaptcha.getResponse = function() { return token; };
    }
""", token)

# Then submit the form normally
driver.find_element(By.CSS_SELECTOR, "form").submit()

Vérifiez ce point avant de chercher un callback fantôme : si data-callback renvoie null et qu'aucun grecaptcha.render() n'apparaît dans les scripts inline, vous êtes dans ce cas de figure.


Le callback d'expiration, ou pourquoi le formulaire se reverrouille

L'attribut data-expired-callback pointe vers une fonction que Google déclenche quand le token arrive en fin de vie. Sur beaucoup de sites, elle redésactive le bouton d'envoi. Un token résolu très en amont du remplissage du formulaire peut donc être annulé sous vos yeux, sans le moindre message d'erreur.

// Check for expired callback
const expiredCallback = document.querySelector('.g-recaptcha')
  ?.getAttribute('data-expired-callback');
console.log('Expired callback:', expiredCallback);

La parade est une question d'ordonnancement : remplissez d'abord les champs, lancez la résolution ensuite, injectez et envoyez dans la foulée. Résoudre au plus près de la soumission coûte quelques secondes d'attente et supprime toute une classe d'échecs difficiles à reproduire.


Scénario : recette d'un portail de rendez-vous francophone

Une équipe QA lyonnaise valide chaque nuit le parcours de prise de rendez-vous de son propre portail, hébergé chez OVHcloud et protégé par reCAPTCHA v2. Le scénario passait en préproduction et échouait en recette : même sitekey, mais le widget y était monté par un composant React, sans data-callback dans le HTML. La méthode 3 a révélé un callback anonyme enregistré au render(), et le repli ___grecaptcha_cfg a rétabli la suite de tests.

Deux réflexes utiles côté exécution. Le dimensionnement d'abord : la facturation CaptchaAI se fait au thread, avec des résolutions illimitées par thread, et le plan BASIC ($15/mois, 5 threads) autorise cinq résolutions en vol simultanées, ce qui suffit à une campagne nocturne de quelques centaines de scénarios. La journalisation ensuite : côté RGPD, conservez l'identifiant de tâche et le statut, pas le token ni les données saisies dans le formulaire de test. La facturation reste en dollars US, quel que soit votre pays.


Checklist de dépannage

Problème Cause probable Correctif
Bouton d'envoi toujours grisé Callback jamais appelé Récupérer le nom dans le DOM et appeler window[nom](token)
ReferenceError: function not defined Callback enfermé dans une closure Passer par le repli ___grecaptcha_cfg
Token injecté, aucune requête AJAX Le callback déclenche l'AJAX, pas la soumission du formulaire Lire le corps de la fonction avant de la remplacer
Le formulaire se reverrouille seul data-expired-callback déclenché Résoudre juste avant l'envoi, pas en début de scénario
Token accepté côté page, refusé côté serveur pageurl différent de l'URL réellement chargée Envoyer driver.current_url, redirections comprises

Questions fréquentes

Faut-il aussi déclencher un événement change sur le textarea ?

Rarement, mais cela ne coûte rien. Les formulaires pilotés par un framework écoutent parfois l'événement plutôt que le callback ; un dispatchEvent(new Event('change', { bubbles: true })) après l'injection lève ce doute en une ligne.

Combien de temps un token reste-t-il valide ?

Environ deux minutes, durée fixée par Google et non par le service de résolution. C'est la raison d'être du callback d'expiration, et pourquoi une résolution lancée trop tôt dans un scénario long est invalidée avant l'envoi.

Le même mécanisme s'applique-t-il à Cloudflare Turnstile ?

Le principe est identique — un champ caché à remplir, puis une fonction à notifier — mais rien ne se transpose tel quel : le nom du champ de réponse et l'attribut de configuration sont propres à Turnstile, et se lisent sur le conteneur du widget. Ne réutilisez jamais les identifiants d'un type pour un autre, c'est une source classique de tokens ignorés.

CaptchaAI prend-il en charge hCaptcha avec cette approche ?

Non, hCaptcha n'est pas pris en charge, pas plus que FunCaptcha. Côté types disponibles, comptez sur reCAPTCHA v2 et v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3 et les CAPTCHA image/OCR ; CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) restent en phase d'évaluation.


Passez du token injecté au formulaire réellement envoyé

Créez votre compte et récupérez votre clé API sur captchaai.com.


Guides associés

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