Use Cases

Automatiser un checkout multi-étapes avec résolution CAPTCHA

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.

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