Un tunnel de commande ne casse presque jamais à la première étape. Il casse à la troisième, quand un défi CAPTCHA s'affiche sur la page de livraison alors que votre script vient de cliquer sur « Continuer ». La règle est simple : traitez chaque écran comme un point de contrôle indépendant, cherchez un défi avant chaque soumission, et injectez le token juste avant le clic. Voici comment câbler ce contrôle dans un automate Selenium en Python qui appelle l'API CaptchaAI, sur vos environnements de recette.
Où le CAPTCHA se déclenche dans un tunnel de commande
Un checkout e-commerce n'expose pas un CAPTCHA unique : il en dissémine plusieurs, de types différents selon le risque visé. Cartographiez-les avant d'écrire la moindre ligne de code.
| Étape | Types de CAPTCHA fréquents | Déclencheur côté site |
|---|---|---|
| Ajout au panier | reCAPTCHA v3 (invisible) | Filtrage du trafic automatisé |
| Connexion ou commande invité | reCAPTCHA v2 | Vérification du compte |
| Adresse de livraison | Cloudflare Turnstile | Rate limiting |
| Page de paiement | reCAPTCHA v2 / v3 | Prévention de la fraude |
| Confirmation de commande | reCAPTCHA v2 | Validation finale |
Ces trois familles sont celles que couvre ce guide, et celles que CaptchaAI résout. Si votre boutique s'appuie sur hCaptcha ou FunCaptcha (Arkose Labs), aucun des deux n'est pris en charge : seul le squelette Selenium reste réutilisable.
Le parcours vu de bout en bout
Cinq écrans, cinq points de contrôle potentiels. À chaque flèche, la même question : « y a-t-il un défi à résoudre avant de soumettre ? »
┌────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ Cart │───▶│ Shipping │───▶│ Payment │───▶│ Review │───▶│ Confirm │
│ │ │ Address │ │ Info │ │ Order │ │ │
└────────┘ └───────────┘ └──────────┘ └─────────┘ └──────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[CAPTCHA?] [CAPTCHA?] [CAPTCHA?] [CAPTCHA?] [CAPTCHA?]
Aucune boutique n'affiche les cinq à la fois. Détectez donc les défis dynamiquement plutôt qu'en dur : un site muet la semaine dernière peut activer Turnstile dès le prochain pic de trafic.
Le squelette de l'automate de checkout
CaptchaSolver envoie la tâche à in.php, puis interroge res.php jusqu'à obtenir le token. CheckoutAutomator pilote le navigateur et appelle _solve_if_captcha() avant chaque soumission.
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class CaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(result["request"])
raise TimeoutError("Timed out")
class CheckoutAutomator:
def __init__(self, api_key):
self.solver = CaptchaSolver(api_key)
self.driver = webdriver.Chrome()
self.wait = WebDriverWait(self.driver, 15)
def _find(self, selector):
return self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
def _click(self, selector):
self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, selector))).click()
def _fill(self, selector, value):
el = self._find(selector)
el.clear()
el.send_keys(value)
def _solve_if_captcha(self):
"""Check for and solve any CAPTCHA on the current page."""
html = self.driver.page_source
page_url = self.driver.current_url
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
if turnstile:
sitekey = turnstile[0].get_attribute("data-sitekey")
token = self.solver.solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return "turnstile"
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha and "recaptcha" in html.lower():
sitekey = recaptcha[0].get_attribute("data-sitekey")
# Check if v3 (invisible)
if "grecaptcha.execute" in html:
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"version": "v3",
"action": "checkout",
}, initial_wait=20)
else:
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
return "recaptcha"
return None
# --- Checkout Steps ---
def add_to_cart(self, product_url, quantity=1):
"""Step 1: Navigate to product and add to cart."""
self.driver.get(product_url)
time.sleep(2)
# Set quantity if field exists
qty_fields = self.driver.find_elements(By.CSS_SELECTOR, "input[name='quantity']")
if qty_fields:
qty_fields[0].clear()
qty_fields[0].send_keys(str(quantity))
self._solve_if_captcha()
self._click("[data-action='add-to-cart'], .add-to-cart, #add-to-cart")
time.sleep(2)
return True
def fill_shipping(self, address):
"""Step 2: Fill shipping address."""
self._solve_if_captcha()
field_map = {
"first_name": "#shipping-first-name, [name='firstName']",
"last_name": "#shipping-last-name, [name='lastName']",
"address": "#shipping-address, [name='address1']",
"city": "#shipping-city, [name='city']",
"state": "#shipping-state, [name='state']",
"zip": "#shipping-zip, [name='postalCode']",
"phone": "#shipping-phone, [name='phone']",
}
for field, selectors in field_map.items():
if field in address:
for selector in selectors.split(", "):
elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
if elements:
elements[0].clear()
elements[0].send_keys(address[field])
break
self._solve_if_captcha()
self._click("[data-step='shipping-submit'], .continue-to-payment")
time.sleep(2)
return True
def fill_payment(self, payment):
"""Step 3: Fill payment information."""
self._solve_if_captcha()
# Handle iframe for card fields (common pattern)
iframes = self.driver.find_elements(By.CSS_SELECTOR, "iframe[name*='card']")
if iframes:
self.driver.switch_to.frame(iframes[0])
self._fill("input[name='cardnumber']", payment["card_number"])
self.driver.switch_to.default_content()
if len(iframes) > 1:
self.driver.switch_to.frame(iframes[1])
self._fill("input[name='exp-date']", payment["expiry"])
self.driver.switch_to.default_content()
if len(iframes) > 2:
self.driver.switch_to.frame(iframes[2])
self._fill("input[name='cvc']", payment["cvv"])
self.driver.switch_to.default_content()
else:
# Direct fields
self._fill("[name='cardNumber'], #card-number", payment["card_number"])
self._fill("[name='expiry'], #card-expiry", payment["expiry"])
self._fill("[name='cvv'], #card-cvv", payment["cvv"])
self._solve_if_captcha()
self._click("[data-step='payment-submit'], .continue-to-review")
time.sleep(2)
return True
def confirm_order(self):
"""Step 4: Review and confirm order."""
self._solve_if_captcha()
self._click("[data-step='confirm'], .place-order, #place-order")
time.sleep(3)
# Check for confirmation
html = self.driver.page_source.lower()
if "order confirmed" in html or "thank you" in html or "confirmation" in html:
return True
return False
def run_checkout(self, product_url, address, payment, quantity=1):
"""Run complete checkout flow."""
steps = [
("Add to cart", lambda: self.add_to_cart(product_url, quantity)),
("Proceed to checkout", lambda: self._click(".checkout-btn, a[href*='checkout']")),
("Fill shipping", lambda: self.fill_shipping(address)),
("Fill payment", lambda: self.fill_payment(payment)),
("Confirm order", lambda: self.confirm_order()),
]
for step_name, step_fn in steps:
print(f"Step: {step_name}...")
try:
result = step_fn()
print(f" ✓ {step_name} complete")
except Exception as e:
print(f" ✗ {step_name} failed: {e}")
return False
return True
def close(self):
self.driver.quit()
Trois détails comptent. Turnstile est cherché en premier via .cf-turnstile, car son conteneur porte lui aussi un data-sitekey qui tromperait un test reCAPTCHA naïf. Le token n'atterrit pas au même endroit selon le type — cf-turnstile-response ou g-recaptcha-response. Et la variante v3 envoie une action (checkout) qui doit correspondre à celle attendue par le site.
Lancer un parcours complet en recette
Le point d'entrée reste banal : une URL produit, une adresse, une carte de test (4111111111111111 chez la plupart des passerelles) et un environnement sandbox — jamais la boutique de production.
automator = CheckoutAutomator("YOUR_API_KEY")
try:
success = automator.run_checkout(
product_url="https://store.example.com/product/widget-pro",
address={
"first_name": "Jane",
"last_name": "Smith",
"address": "123 Test Street",
"city": "San Francisco",
"state": "CA",
"zip": "94102",
"phone": "415-555-0100",
},
payment={
"card_number": "4111111111111111",
"expiry": "12/26",
"cvv": "123",
},
quantity=1,
)
print(f"Checkout {'succeeded' if success else 'failed'}")
finally:
automator.close()
Le finally n'est pas décoratif : un parcours qui échoue au paiement laisse sinon un navigateur ouvert.
Cas concret : la recette d'une boutique francophone
Prenons une équipe QA qui recette une boutique livrant en France, en Belgique et au Québec, avec un runner hébergé chez OVHcloud. Le parcours de référence enchaîne un reCAPTCHA v3 invisible au panier, un Cloudflare Turnstile sur la livraison et un reCAPTCHA v2 sur la confirmation.
Le budget temps découle des plafonds publiés : Turnstile en moins de 10 s, reCAPTCHA v3 en moins de 4 s, reCAPTCHA v2 en moins de 60 s. Comptez jusqu'à une bonne minute de résolution cumulée par parcours : un WebDriverWait de 15 s suffit pour le DOM, mais le timeout global du scénario doit accepter ce cumul.
Côté données, restez sobres : des adresses de test synthétiques, pas d'export de clients réels en recette. C'est la lecture RGPD la plus simple à défendre en revue interne.
Les cas limites qui font échouer un parcours
Un défi apparaît après une erreur de validation
Cas classique : le formulaire de livraison est refusé pour un code postal mal formaté, la page se recharge, et un CAPTCHA accompagne désormais le formulaire. Enveloppez l'étape dans une nouvelle tentative qui repasse par la détection.
def retry_step_with_captcha(self, step_fn, max_retries=2):
for attempt in range(max_retries + 1):
try:
return step_fn()
except Exception:
if attempt < max_retries:
self._solve_if_captcha()
continue
raise
Sessions et cookies entre les étapes
Certaines boutiques ne présentent un défi qu'à la première requête d'une session. Relancer le navigateur entre deux étapes le fait réapparaître à chaque fois, et fait souvent perdre le panier : gardez la même instance, ou réinjectez les cookies.
Champs de formulaire chargés après coup
Les tunnels modernes injectent les champs de paiement en JavaScript, souvent dans une iframe. Attendez un indicateur propre à l'étape plutôt qu'un sleep calibré au jugé.
def wait_for_step(self, indicator_selector, timeout=15):
"""Wait for a step to fully load before proceeding."""
WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((By.CSS_SELECTOR, indicator_selector))
)
Threads, volume et coût d'un parcours
CaptchaAI facture au thread simultané, pas à la résolution : un thread correspond à un CAPTCHA en cours et se libère dès le token retourné, avec un nombre de résolutions illimité sur le mois. Ce n'est donc pas le nombre de parcours qui dimensionne votre plan, mais le nombre de défis simultanés — et cinq parcours parallèles ne mobilisent jamais cinq threads en continu, puisque le remplissage des formulaires n'appelle pas l'API. Le plan BASIC ($15/mois, 5 threads) absorbe une campagne nocturne de quelques dizaines de parcours ; passez à STANDARD ($30/mois, 15 threads) quand plusieurs scénarios se chevauchent en intégration continue. La facturation se fait en dollars US.
Dépannage
| Problème | Cause probable | Correctif |
|---|---|---|
| Token refusé au moment du paiement | Token expiré entre deux écrans | Résolvez le défi juste avant le clic de soumission |
| Les champs carte restent vides | Champs Stripe/Braintree dans une iframe | Basculez sur l'iframe avec switch_to.frame avant de remplir |
| Le checkout renvoie vers la connexion | Session expirée ou cookies perdus | Ajoutez une connexion en amont, gardez le même navigateur |
| La commande échoue sans message | Champ obligatoire non rempli | Relisez les erreurs du DOM après chaque étape |
| Score reCAPTCHA v3 trop bas | action différente de celle attendue |
Alignez action sur la valeur du site |
FAQ
Combien de threads faut-il pour tester plusieurs parcours en parallèle ?
Comptez un thread par défi réellement simultané, pas un thread par parcours : cinq scénarios lancés ensemble se croisent rarement sur la même seconde. Le plan BASIC ($15/mois, 5 threads) suffit à la plupart des suites de recette.
CaptchaAI résout-il hCaptcha sur une page de paiement ?
Non — hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs). Sur un tunnel de commande, les types couverts sont reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest v3 et les CAPTCHA image/OCR.
Pourquoi le token est-il rejeté alors que la résolution a réussi ?
Une question de fraîcheur ou de contexte : un token obtenu au panier n'est plus valable trois écrans plus loin, et un token résolu sur une pageurl différente de la page affichée sera refusé. Résolvez au dernier moment, sur l'URL exacte que montre le navigateur.
Puis-je utiliser de vraies données clients dans mes scénarios ?
Évitez-le. Des adresses et des cartes de test couvrent tous les cas fonctionnels sans disséminer de données personnelles dans les captures, les logs et les rapports — un réflexe RGPD élémentaire.
Guides connexes
Automatisez vos tests de commande — gérez les CAPTCHA avec CaptchaAI.