Sur un site qui expose deux fournisseurs, une seule approche tient dans la durée : ne rien décider à l'avance. À chaque chargement de page, lisez le HTML, identifiez le widget réellement rendu, puis appelez la méthode d'API correspondante. Un script figé sur userrecaptcha s'arrête net le jour où la page de paiement bascule sur Cloudflare Turnstile, et l'erreur ne dit jamais « mauvais fournisseur » : elle dit que le token est refusé.
Le cas n'a rien d'exotique : une même URL peut servir reCAPTCHA v2 à une session et Turnstile à la suivante. Traitez le type de CAPTCHA comme une donnée d'exécution, jamais comme une constante de configuration.
Reconnaître le widget dans le HTML
Trois signaux suffisent, tous présents dans le HTML servi avant toute exécution de script : la classe CSS du conteneur, l'URL du script chargé et le nom du champ caché qui recevra le token. Vérifiez la classe en premier : data-sitekey seul est ambigu, les deux fournisseurs l'utilisent.
| Fournisseur | Marqueur HTML | Script chargé | Champ de réponse |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
La ligne hCaptcha sert au diagnostic, pas à la résolution : hCaptcha n'est pas pris en charge par CaptchaAI. Si ce marqueur remonte, la page sort du périmètre couvert et votre workflow doit le signaler plutôt que d'envoyer une tâche vouée à l'échec.
Pourquoi un même site expose deux fournisseurs
La raison du mélange dicte la fréquence de détection : une migration se stabilise, un test A/B non.
| Scénario | Ce que vous observez |
|---|---|
| Pages différentes, fournisseurs différents | Connexion = reCAPTCHA v2, paiement = Turnstile |
| Test A/B entre fournisseurs | La même page affiche aléatoirement l'un ou l'autre |
| Migration en cours | Les anciennes pages gardent reCAPTCHA, les nouvelles passent à Turnstile |
| Repli après incident | Le fournisseur principal ne répond plus, le secondaire prend le relais |
| Variation régionale | reCAPTCHA v2 pour l'Amérique du Nord, Turnstile pour l'Europe |
Détecter et résoudre reCAPTCHA v2 ou Turnstile en Python
Le script ci-dessous récupère la page avec la session en cours, déduit le fournisseur à partir des marqueurs HTML, puis construit le payload adapté. Les paramètres diffèrent — reCAPTCHA v2 attend googlekey, Turnstile attend sitekey — mais les endpoints in.php et res.php restent les mêmes. Le polling tourne à 5 secondes d'intervalle, cohérent avec les temps de résolution annoncés : moins de 10 s pour Turnstile, jusqu'à 60 s pour reCAPTCHA v2.
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://example.com/login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
Notez que process_page renvoie le nom du champ en même temps que le token. Ce couple évite l'erreur la plus coûteuse du multi-fournisseurs : un token valide posté dans le mauvais champ.
Détection dynamique côté JavaScript
Même logique en Node.js ou Playwright. La troisième regex rattrape les widgets rendus par script, absents du HTML initial. Sur les pages construites côté client, gardez ce filet : sans lui, la détection renvoie null alors que le CAPTCHA est bien là.
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://example.com/login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
Scénario : une boutique européenne en pleine migration
Une équipe QA teste le tunnel d'achat d'une boutique hébergée chez OVHcloud, avec des runners dans la région AWS eu-west-3 (Paris). La connexion sert encore reCAPTCHA v2, mais depuis la refonte, le paiement charge Turnstile pour les visiteurs européens — un choix souvent motivé par la limitation des transferts de données personnelles hors UE, argument classique des arbitrages RGPD.
Résultat : la suite de tests passe la connexion et échoue à l'étape de paiement, sans qu'aucune ligne de log ne mentionne Turnstile. La détection dynamique règle le problème sans toucher au reste du parcours. Appliquez le même réflexe RGPD à vos traces : journalisez le fournisseur détecté et le sitekey, jamais le contenu du formulaire.
Dimensionner vos threads pour deux fournisseurs
La facturation CaptchaAI repose sur les threads simultanés, avec un nombre de résolutions illimité par thread : gérer deux fournisseurs ne coûte rien de plus. Ce qui change, c'est la durée d'occupation d'un thread — moins de 10 s pour Turnstile, jusqu'à 60 s pour reCAPTCHA v2. Dimensionnez sur le fournisseur le plus lent, pas sur la moyenne. Pour une suite de tests nocturne sur quelques centaines de parcours, BASIC ($15/mois, 5 threads) suffit. Une équipe qui exécute des campagnes en parallèle sur plusieurs environnements sera plus à l'aise avec STANDARD ($30/mois, 15 threads), et une automatisation continue à fort volume relève d'ADVANCE ($90/mois, 50 threads). La facturation est en dollars américains.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| Mauvais fournisseur détecté | La regex s'accroche à data-sitekey sans vérifier la classe |
Testez d'abord g-recaptcha et cf-turnstile, l'attribut ensuite |
| Token refusé alors que la résolution a réussi | Le token est posté dans le champ de l'autre fournisseur | Renvoyez toujours le couple fournisseur + champ : g-recaptcha-response ou cf-turnstile-response |
| Le type change d'une visite à l'autre | Test A/B ou routage régional | Détectez à chaque requête de page, ne mettez jamais le fournisseur en cache |
| Deux widgets détectés sur la même page | L'un des deux est masqué ou inactif | Contrôlez la visibilité de l'élément et ne résolvez que le widget affiché |
| Aucun marqueur trouvé | Widget injecté par script après le chargement | Cherchez grecaptcha.render() ou turnstile.render() dans les scripts, ou lisez le DOM après rendu |
FAQ
Quel champ de formulaire faut-il remplir selon le fournisseur ?
g-recaptcha-response pour reCAPTCHA v2, cf-turnstile-response pour Cloudflare Turnstile. Les noms ne sont pas interchangeables : un token Turnstile déposé dans le champ reCAPTCHA est rejeté côté serveur sans message explicite.
CaptchaAI utilise-t-il les mêmes appels pour les deux fournisseurs ?
Les endpoints sont identiques (in.php puis res.php), seule la valeur de method diffère : userrecaptcha avec googlekey pour reCAPTCHA v2, turnstile avec sitekey pour Turnstile. Le détecteur se contente de choisir la bonne paire.
CaptchaAI prend-il le relais si le site bascule sur hCaptcha ?
Non — hCaptcha n'est pas pris en charge par CaptchaAI. Faites remonter le cas comme une erreur métier explicite dans vos logs plutôt que comme un échec de résolution, et documentez la page concernée pour l'équipe qui suit la couverture des tests.
Faut-il un plan plus élevé pour gérer deux fournisseurs ?
Non. Le nombre de résolutions est illimité par thread, donc le fournisseur n'entre pas dans le calcul. Ne redimensionnez que si vos parcours les plus lents saturent vos threads en simultané.
Articles connexes
- Résoudre le callback reCAPTCHA v2 via l'API
- Distinguer Cloudflare Challenge de Turnstile
- GeeTest v3 face à Cloudflare Turnstile
- Gérer plusieurs CAPTCHA sur une seule page
- Formulaires séquentiels et enchaînements de CAPTCHA
- Détecter et injecter un CAPTCHA dans une modale
Prochaines étapes
Branchez la détection dynamique sur votre pipeline, puis récupérez votre clé API CaptchaAI et vérifiez sur deux parcours réels que chaque token part dans le bon champ.