Même page, même formulaire, deux résultats : le parcours passe en Chrome desktop et échoue dès que vous basculez en émulation iPhone. Le CAPTCHA n'y est le plus souvent pour rien — ce qui casse, c'est la cohérence du contexte mobile (User-Agent, viewport, événements tactiles) entre la résolution du token et l'envoi du formulaire. La séquence, elle, ne bouge pas : lire le sitekey, l'envoyer à l'API, injecter le token dans g-recaptcha-response, soumettre.
CaptchaAI résout le défi sans savoir si la session vient d'un téléphone ou d'un poste fixe. Ce guide construit deux chaînes complètes : Playwright en Python, Selenium en JavaScript.
Ce qu'il faut préparer
- Une clé API CaptchaAI et un solde vérifié avant la campagne de tests.
- Python 3.10+ avec
playwrightethttpx, ou Node.js 18+ avecselenium-webdriveretaxios. - Une page que vous êtes autorisé à automatiser (votre staging) et un profil d'appareil fixé à l'avance.
Le scénario : un tunnel de commande responsive
Une équipe QA à Lyon valide le tunnel de commande d'un site marchand responsive. Le serveur détecte les User-Agent mobiles et sert une mise en page différente du desktop : widget reCAPTCHA v2 en version compacte, iframe redimensionnée, bouton de validation au bas de l'écran. Les runners d'intégration continue tournent sur des instances Scaleway en région parisienne.
Point de conformité : les captures d'écran de formulaires pré-remplis contiennent des données personnelles. Purgez-les en fin de job, au titre du RGPD.
Méthode 1 — Playwright et l'émulation d'appareil (Python)
Playwright embarque un catalogue de profils d'appareils : un seul appel à p.devices aligne le viewport, le pixel ratio, le User-Agent et la prise en charge du tactile. Aucun téléphone physique n'est nécessaire.
Le script envoie le sitekey à in.php, interroge res.php toutes les 5 secondes jusqu'au token, l'injecte dans la textarea, déclenche le callback s'il existe, puis soumet le formulaire.
# playwright_mobile_captcha.py
import asyncio
import httpx
from playwright.async_api import async_playwright
API_KEY = "YOUR_API_KEY"
async def solve_recaptcha(sitekey: str, pageurl: str) -> str:
"""Submit reCAPTCHA v2 to CaptchaAI and poll for result."""
async with httpx.AsyncClient(timeout=180) as client:
# Submit task
resp = await client.get(
"https://ocr.captchaai.com/in.php",
params={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": "1",
},
)
result = resp.json()
if result["status"] != 1:
raise Exception(f"Submit failed: {result['request']}")
task_id = result["request"]
# Poll for result
for _ in range(30):
await asyncio.sleep(5)
poll = await client.get(
"https://ocr.captchaai.com/res.php",
params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": "1",
},
)
poll_result = poll.json()
if poll_result["status"] == 1:
return poll_result["request"]
if poll_result["request"] != "CAPCHA_NOT_READY":
raise Exception(f"Solve failed: {poll_result['request']}")
raise Exception("Polling timeout")
async def main():
async with async_playwright() as p:
# Launch with iPhone 13 emulation
iphone = p.devices["iPhone 13"]
browser = await p.chromium.launch(headless=False)
context = await browser.new_context(**iphone)
page = await context.new_page()
await page.goto("https://example.com/mobile-form")
await page.wait_for_selector(".g-recaptcha", timeout=10000)
# Extract sitekey
sitekey = await page.get_attribute(".g-recaptcha", "data-sitekey")
pageurl = page.url
print(f"Found sitekey: {sitekey}")
# Solve via CaptchaAI
token = await solve_recaptcha(sitekey, pageurl)
print(f"Token received: {token[:50]}...")
# Inject token
await page.evaluate(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
document.getElementById('g-recaptcha-response').style.display = '';
""")
# Trigger callback if it exists
await page.evaluate(f"""
try {{
const clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach(k => {{
Object.keys(clients[k]).forEach(j => {{
if (clients[k][j] && clients[k][j].callback) {{
clients[k][j].callback('{token}');
}}
}});
}});
}} catch(e) {{}}
""")
# Submit form
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle")
print("Form submitted successfully")
await browser.close()
asyncio.run(main())
Deux détails pèsent lourd. Le polling laisse 30 tentatives espacées de 5 secondes : reCAPTCHA v2 est annoncé en moins de 60 s, la marge absorbe les pics de charge. Et sur beaucoup de formulaires mobiles, remplir la textarea ne suffit pas — callback(token) débloque le bouton de validation.
Méthode 2 — Selenium et l'émulation mobile de Chrome (JavaScript)
Si votre suite est déjà en Selenium, inutile de changer d'outil : l'option setMobileEmulation du driver Chrome pousse les métriques d'appareil et le User-Agent via le protocole DevTools.
// selenium_mobile_captcha.js
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function solveCaptcha(sitekey, pageurl) {
// Submit task
const submitResp = await axios.get('https://ocr.captchaai.com/in.php', {
params: {
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
},
});
if (submitResp.data.status !== 1) {
throw new Error(`Submit failed: ${submitResp.data.request}`);
}
const taskId = submitResp.data.request;
// Poll for result
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
});
if (pollResp.data.status === 1) return pollResp.data.request;
if (pollResp.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve failed: ${pollResp.data.request}`);
}
}
throw new Error('Polling timeout');
}
async function main() {
// Configure mobile emulation
const mobileEmulation = {
deviceMetrics: { width: 390, height: 844, pixelRatio: 3.0 },
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) ' +
'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
};
const options = new chrome.Options();
options.setMobileEmulation(mobileEmulation);
const driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.build();
try {
await driver.get('https://example.com/mobile-form');
await driver.wait(until.elementLocated(By.css('.g-recaptcha')), 10000);
// Extract sitekey
const captchaEl = await driver.findElement(By.css('.g-recaptcha'));
const sitekey = await captchaEl.getAttribute('data-sitekey');
const pageurl = await driver.getCurrentUrl();
console.log(`Sitekey: ${sitekey}`);
// Solve CAPTCHA
const token = await solveCaptcha(sitekey, pageurl);
console.log(`Token: ${token.substring(0, 50)}...`);
// Inject token
await driver.executeScript(`
document.getElementById('g-recaptcha-response').value = arguments[0];
`, token);
// Submit form
await driver.findElement(By.css('button[type="submit"]')).click();
console.log('Form submitted');
} finally {
await driver.quit();
}
}
main().catch(console.error);
Nuance par rapport à Playwright : deviceMetrics change la fenêtre et le pixel ratio, pas la nature des événements émis. Si la page attend un touchstart, passez par deviceName ou activez le tactile via DevTools.
Desktop et mobile : ce qui change réellement
| Facteur | Desktop | Mobile | Conséquence sur le CAPTCHA |
|---|---|---|---|
| User-Agent | Chrome ou Firefox desktop | Safari mobile, Chrome Android | Configuration CAPTCHA parfois différente selon l'appareil |
| Viewport | 1920 × 1080 et au-delà | 390 × 844 px | Widget rendu en mise en page compacte |
| Interaction | Événements souris | Événements tactiles | L'affichage peut dépendre du type d'interaction |
| Réseau | Haut débit | 4G/5G simulés | Timeouts à élargir |
Émulation ou appareil réel : comment trancher
L'émulation gagne partout où la vitesse compte : démarrage en quelques secondes, aucun matériel dédié, parallélisation immédiate. Sa limite : elle change ce que la page voit, pas ce que le système expose — navigator.platform trahit le poste sous-jacent.
Les appareils réels, pilotés par ADB ou Appium, restent adaptés aux recettes finales. Le compromis courant : émulation à chaque commit, appareils réels avant livraison.
Dépannage du CAPTCHA en contexte mobile
| Problème | Cause | Correctif |
|---|---|---|
| Le widget ne s'affiche pas en émulation | La page teste navigator.platform ou l'absence de tactile |
Forcer l'override de plateforme et activer le tactile via DevTools |
Textarea g-recaptcha-response introuvable |
La mise en page mobile rend le widget autrement | Cibler la textarea par son attribut name, pas par son id |
| Token injecté mais formulaire rejeté | Le serveur compare le User-Agent de la résolution et celui de l'envoi | Garder le même User-Agent de bout en bout, proxy compris |
| Chargements très lents | Pages lourdes en images et en polices | Lancer Chrome avec --disable-images |
Threads, volume et budget
La facturation CaptchaAI se fait au thread concurrent, pas à la résolution : un thread traite un CAPTCHA à la fois puis enchaîne, sans plafond journalier ni surcoût par type. Raisonnez en parallélisme, pas en nombre de tests.
Trois workers Playwright qui résolvent chacun un reCAPTCHA v2 au même instant consomment trois threads : le plan BASIC ($15/mois, 5 threads) suffit. Une exécution nocturne qui rejoue quarante parcours en parallèle relève plutôt d'ADVANCE ($90/mois, 50 threads). Facturation en dollars US, résolutions illimitées dans chaque thread.
Ce que CaptchaAI prend en charge ici
Les types utiles à un parcours mobile sont couverts : reCAPTCHA v2 (Invisible, Enterprise, variante callback), reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA image et OCR, grilles d'images et BLS — courant sur les portails de rendez-vous consultés depuis le Maghreb.
Deux limites avant d'écrire votre plan de test : hCaptcha et FunCaptcha (Arkose Labs) ne sont pas pris en charge, et GeeTest v4 est annoncé comme à venir, pas disponible. CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) restent hors du chemin critique.
FAQ
Pourquoi le CAPTCHA ne s'affiche-t-il pas en émulation mobile ?
La page détecte l'émulation avant de rendre le widget : navigator.platform renvoie le système du poste et aucun événement tactile n'est disponible. Forcez l'override de plateforme, activez le tactile, puis rechargez avant de chercher le sitekey.
Pourquoi le formulaire est-il refusé alors que le token a été obtenu ?
Le serveur compare le contexte de la résolution et celui de l'envoi. Un User-Agent réécrit entre les deux étapes, une session repartie de zéro ou une adresse IP différente suffisent à invalider la soumission.
Combien de threads prévoir pour une suite de tests mobiles ?
Autant que de scénarios exécutés en parallèle, pas autant que de tests écrits. Cinq navigateurs émulés lancés simultanément consomment cinq threads au pic ; les résolutions restent illimitées dans le plan.
CaptchaAI prend-il en charge hCaptcha sur les pages mobiles ?
Non, hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs), sur mobile comme sur desktop. Si votre page cible en dépend, prévoyez un environnement de test qui neutralise le défi.
Articles connexes
- déclencher le callback reCAPTCHA v2 depuis l'API
- orchestrer CaptchaAI sans code avec Zapier
- gérer reCAPTCHA v2 et Turnstile sur un même site
Prochaines étapes
Validez d'abord le token sur une page de staging, puis branchez la suite complète — récupérez votre clé API CaptchaAI et alignez le User-Agent de vos runners sur le profil d'appareil testé.
Guides associés :