Casos de Uso

Automatización de pago en varios pasos con resolución CAPTCHA

Un checkout no es una sola página: es una secuencia — carrito, envío, pago y confirmación — y cualquiera de esos pasos puede lanzar un CAPTCHA que detiene tu prueba de extremo a extremo. La solución no es desactivar la seguridad del sitio, sino detectar y resolver el CAPTCHA en el paso exacto donde aparece, sin abandonar la sesión del navegador. En esta guía montamos un automatizador en Python con Selenium que recorre cada etapa del flujo y llama a la API de CaptchaAI solo cuando encuentra un reCAPTCHA v2, un reCAPTCHA v3 invisible o un Cloudflare Turnstile.


En qué etapas del checkout aparece el CAPTCHA

Etapa Tipos de CAPTCHA habituales Por qué aparece
Añadir al carrito reCAPTCHA v3 (invisible) Prevención de bots
Login o compra como invitado reCAPTCHA v2 Verificación de cuenta
Dirección de envío Turnstile Límite de solicitudes
Página de pago reCAPTCHA v2/v3 Prevención de fraude
Confirmación del pedido reCAPTCHA v2 Validación final

No todos los sitios protegen las cinco etapas, y algunos mueven el CAPTCHA de un paso a otro entre releases. Por eso conviene comprobar cada página en lugar de asumir dónde estará el desafío.


Mapa del flujo de checkout

┌────────┐    ┌───────────┐    ┌──────────┐    ┌─────────┐    ┌──────────┐
│  Cart  │───▶│  Shipping │───▶│ Payment  │───▶│ Review  │───▶│ Confirm  │
│        │    │  Address  │    │  Info    │    │  Order  │    │          │
└────────┘    └───────────┘    └──────────┘    └─────────┘    └──────────┘
     │              │               │               │              │
     ▼              ▼               ▼               ▼              ▼
  [CAPTCHA?]    [CAPTCHA?]     [CAPTCHA?]      [CAPTCHA?]     [CAPTCHA?]

Cada nodo del flujo hace la misma pregunta antes de avanzar: ¿hay un CAPTCHA en esta página? Esa comprobación es la que centralizamos en un solo método reutilizable.


El automatizador de checkout en Python

Clase automatizadora paso a paso

El núcleo son dos clases: CaptchaSolver, que envía la tarea a in.php y consulta el resultado en res.php con un sondeo sencillo, y CheckoutAutomator, que conduce el navegador y llama a _solve_if_captcha() en cada etapa. Ese método inspecciona el HTML de la página actual, distingue entre Turnstile y reCAPTCHA (y, dentro de reCAPTCHA, entre v2 y el v3 invisible que se dispara con grecaptcha.execute) e inyecta el token en el campo correcto.

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()

El sondeo empieza con una espera inicial más larga para el reCAPTCHA v3 (initial_wait=20), porque las tareas basadas en puntuación tardan algo más en resolverse que un v2 clásico.


Cómo ejecutar el flujo completo

Con la clase lista, un checkout de prueba completo cabe en unas pocas líneas. Usa siempre datos de prueba y una URL de staging que controles; el bloque finally cierra el navegador aunque un paso falle a mitad del flujo.

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()

Casos límite que rompen la automatización

Un CAPTCHA aparece tras un error de validación

Cuando un campo se rechaza (un código postal inválido, por ejemplo), muchos sitios inyectan un CAPTCHA nuevo al reintentar. Envuelve el paso en una función de reintento que vuelva a resolver el desafío antes de repetir la acción.

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

CAPTCHA ligados a la sesión

Algunos sitios solo muestran el CAPTCHA en la primera solicitud de cada sesión. Mantén las cookies del navegador para conservar la sesión entre pasos y evitar que cada página vuelva a pedir verificación.

Campos de formulario que cargan tarde

Los checkouts de una sola página suelen renderizar cada bloque de forma dinámica. Espera a que el indicador del paso esté presente antes de rellenarlo, en lugar de fiarte de un time.sleep fijo.

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))
    )

Diagnóstico rápido de errores frecuentes

Problema Causa Solución
El token se rechaza en el pago El token caducó Resuelve el CAPTCHA justo antes de pulsar enviar
Los campos de la tarjeta no se rellenan iframe de Stripe/Braintree Cambia al iframe antes de rellenar
El pago redirige al login La sesión expiró Añade un paso de inicio de sesión antes del pago
El pedido falla en silencio Faltan campos obligatorios Revisa los mensajes de error tras cada paso

Ejecutar pruebas de checkout en paralelo

Imagina un equipo de QA en Bogotá que valida el checkout de su tienda antes de cada lanzamiento: cinco navegadores recorriendo el flujo a la vez, y cada uno tropezando con un CAPTCHA en el paso de pago. Como CaptchaAI factura por thread concurrente — no por resolución —, ese paralelismo es predecible: cada CAPTCHA en curso ocupa un thread y, al terminar, lo libera para el siguiente. El plan BASIC ($15/mes, 5 threads) encaja en una suite pequeña; si tu batería de pruebas crece, STANDARD ($30/mes, 15 threads) amplía el margen sin cambiar una línea de código. Para agencias y freelancers que facturan en monedas locales volátiles, ese costo mensual fijo en USD es más fácil de presupuestar que un pago por resolución.


Preguntas frecuentes

¿Puedo probar el checkout en modo sandbox sin cobros reales?

Sí. La mayoría de las plataformas de e-commerce ofrecen un modo sandbox con números de tarjeta de prueba. CaptchaAI resuelve el mismo reCAPTCHA o Turnstile tanto en el entorno de prueba como en producción.

¿CaptchaAI resuelve el reCAPTCHA v3 invisible del carrito?

Sí. El reCAPTCHA v3 no muestra un widget: se dispara con grecaptcha.execute y devuelve una puntuación. El automatizador lo detecta en el HTML y envía la tarea con version: v3 y una action, sin intervención manual.

¿Qué plan necesito para lanzar varios checkouts a la vez?

Depende del paralelismo. CaptchaAI factura por thread concurrente, no por resolución: cada CAPTCHA en curso ocupa un thread. El plan BASIC ($15/mes, 5 threads) cubre pruebas pequeñas, y STANDARD ($30/mes, 15 threads) da margen para una suite de regresión más amplia.

¿Cómo evito que el token caduque antes de confirmar el pedido?

Resuelve el CAPTCHA en el último momento posible dentro de cada paso, justo antes de enviar el formulario. Los tokens tienen una vida corta: si resuelves al principio de un paso largo, es probable que ya no sea válido al confirmar.


Guías relacionadas


Automatiza las pruebas de tu checkout: resuelve el CAPTCHA con CaptchaAI.

Los comentarios están deshabilitados para este artículo.