Casos de Uso

Pruebas de endpoints API con CAPTCHA en formularios web

¿Tu suite de pruebas automatizadas se rompe en cuanto un formulario muestra un reCAPTCHA o un widget de Cloudflare Turnstile? La solución no es abrir un navegador ni desactivar la protección en staging: es obtener un token de CAPTCHA válido por API y enviarlo al endpoint, igual que un usuario legítimo.

Con CaptchaAI resuelves el CAPTCHA en segundos, inyectas el token en el payload y validas la respuesta del backend sin arrancar Chrome ni Selenium. Es más rápido, más estable y mucho más fácil de mantener en CI/CD que una prueba basada en navegador.


Cuándo conviene probar por API y no por navegador

No todas las pruebas necesitan un navegador. Si lo que verificas es la lógica del servidor —no el renderizado del formulario— saltártelo ahorra segundos por caso y elimina una gran fuente de inestabilidad. Estos son los escenarios donde el enfoque por API rinde más:

  • Validación del backend: comprueba que el servidor acepta un token real y rechaza los inválidos.
  • Pruebas de carga: lanza cientos de solicitudes contra endpoints con CAPTCHA sin abrir un navegador por cada una.
  • Integración en CI/CD: valida las APIs de envío de formularios como un paso más del pipeline.
  • Manejo de errores: confirma que los mensajes son correctos ante tokens caducados, ausentes o malformados.

Piensa en un equipo de QA que mantiene el formulario de alta de una plataforma SaaS con clientes en España y México. El endpoint de registro usa reCAPTCHA v3 y, hasta ahora, cada release exigía validación manual: el navegador headless tropezaba con el widget.

Al mover esa verificación a una prueba por API —resolver el token con CaptchaAI y enviarlo al endpoint— ese caso pasó de dos minutos frágiles a quince segundos que corren en cada pull request.


Cómo funciona el flujo

El patrón es siempre el mismo, cuatro pasos:

  • Resolver: pides el token del CAPTCHA a la API de CaptchaAI.
  • Construir: montas el payload con el token en su campo.
  • Enviar: haces POST al endpoint, como el navegador.
  • Validar: compruebas el código de estado y el cuerpo de la respuesta.
┌──────────┐     ┌────────────┐     ┌──────────────┐     ┌──────────────┐
│ Solve    │────▶│ Build      │────▶│ POST to      │────▶│ Validate     │
│ CAPTCHA  │     │ Request    │     │ Endpoint     │     │ Response     │
│ (API)    │     │ Payload    │     │              │     │              │
└──────────┘     └────────────┘     └──────────────┘     └──────────────┘

En la mayoría de las pruebas de endpoints no hace falta navegador: el token viaja como un campo más del formulario.


Implementación en Python

La solución se apoya en dos clases pequeñas: una pide tokens a CaptchaAI y otra arma la solicitud, la envía al endpoint y comprueba la respuesta. Cópialas tal cual o adáptalas a tu framework de pruebas.

Proveedor de tokens CAPTCHA

El proveedor encapsula el ciclo completo: envía la tarea a in.php y consulta el resultado en res.php hasta que el token está listo. Un método cubre reCAPTCHA v2 y v3 —la diferencia es el parámetro version— y otro resuelve Turnstile.

import time
import requests

class TokenProvider:
    BASE = "https://ocr.captchaai.com"

    def __init__(self, api_key):
        self.api_key = api_key

    def get_recaptcha_token(self, sitekey, pageurl, version="v2"):
        params = {
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
        }
        if version == "v3":
            params["version"] = "v3"
            params["action"] = "submit"
        return self._solve(params, initial_wait=15 if version == "v3" else 10)

    def get_turnstile_token(self, sitekey, pageurl):
        return self._solve({
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": pageurl,
        })

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

La espera inicial (initial_wait) es más larga para v3 porque tarda algo más; luego el bucle consulta el resultado cada cinco segundos y lanza una excepción si algo falla o se agota el tiempo de espera.

Motor de pruebas de endpoints

La clase EndpointTester recibe una configuración por caso —URL, tipo de CAPTCHA, sitekey, payload y respuesta esperada—, resuelve el token, lo inyecta en el campo correcto y valida la respuesta. También comprueba tokens inválidos y ausentes.

import json
import time

class EndpointTester:
    def __init__(self, api_key):
        self.token_provider = TokenProvider(api_key)
        self.session = requests.Session()
        self.results = []

    def test_endpoint(self, config):
        """
        config: {
            "name": "test name",
            "url": "endpoint URL",
            "method": "POST",
            "captcha_type": "recaptcha_v2" | "recaptcha_v3" | "turnstile",
            "sitekey": "...",
            "pageurl": "...",
            "captcha_field": "g-recaptcha-response",
            "payload": { ... form data ... },
            "expected_status": 200,
            "expected_contains": "success",
        }
        """
        start = time.time()
        result = {"name": config["name"], "passed": False}

        try:
            # Get CAPTCHA token
            captcha_type = config.get("captcha_type", "recaptcha_v2")
            if captcha_type == "recaptcha_v2":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"]
                )
            elif captcha_type == "recaptcha_v3":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"], version="v3"
                )
            elif captcha_type == "turnstile":
                token = self.token_provider.get_turnstile_token(
                    config["sitekey"], config["pageurl"]
                )
            else:
                raise ValueError(f"Unknown captcha type: {captcha_type}")

            # Build payload
            payload = {**config.get("payload", {})}
            captcha_field = config.get("captcha_field", "g-recaptcha-response")
            payload[captcha_field] = token

            # Submit request
            method = config.get("method", "POST").upper()
            headers = config.get("headers", {})

            if config.get("json_body"):
                resp = self.session.request(
                    method, config["url"], json=payload, headers=headers
                )
            else:
                resp = self.session.request(
                    method, config["url"], data=payload, headers=headers
                )

            # Validate response
            result["status_code"] = resp.status_code
            result["response_length"] = len(resp.text)
            result["elapsed"] = round(time.time() - start, 2)

            # Check expected status
            expected_status = config.get("expected_status", 200)
            if resp.status_code != expected_status:
                result["error"] = f"Expected {expected_status}, got {resp.status_code}"
                self.results.append(result)
                return result

            # Check expected content
            expected = config.get("expected_contains")
            if expected and expected.lower() not in resp.text.lower():
                result["error"] = f"Response missing: '{expected}'"
                self.results.append(result)
                return result

            result["passed"] = True

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_invalid_token(self, config):
        """Test that endpoint rejects invalid CAPTCHA tokens."""
        invalid_config = {**config}
        invalid_config["name"] = f"{config['name']} (invalid token)"

        # Override with fake token
        payload = {**config.get("payload", {})}
        captcha_field = config.get("captcha_field", "g-recaptcha-response")
        payload[captcha_field] = "INVALID_TOKEN_12345"

        start = time.time()
        result = {"name": invalid_config["name"], "passed": False}

        try:
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            # Should reject — 4xx or error message
            if resp.status_code >= 400 or "error" in resp.text.lower() or "invalid" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted invalid CAPTCHA token"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_missing_token(self, config):
        """Test that endpoint rejects missing CAPTCHA token."""
        start = time.time()
        result = {"name": f"{config['name']} (missing token)", "passed": False}

        try:
            payload = config.get("payload", {})
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            if resp.status_code >= 400 or "captcha" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted request without CAPTCHA"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def run_suite(self, configs):
        """Run a full test suite against multiple endpoints."""
        for config in configs:
            self.test_endpoint(config)
            self.test_invalid_token(config)
            self.test_missing_token(config)
        return self.report()

    def report(self):
        passed = sum(1 for r in self.results if r["passed"])
        total = len(self.results)
        lines = [f"Endpoint Tests: {passed}/{total} passed", "=" * 50]
        for r in self.results:
            status = "PASS" if r["passed"] else "FAIL"
            elapsed = r.get("elapsed", "?")
            lines.append(f"  [{status}] {r['name']} ({elapsed}s)")
            if r.get("error"):
                lines.append(f"         Error: {r['error']}")
        return "\n".join(lines)

Fíjate en captcha_field: cada tipo usa su propio nombre de campo (g-recaptcha-response para reCAPTCHA, cf-turnstile-response para Turnstile). Enviar el token en el campo equivocado es un error común: el backend lo tratará como si faltara.


Ejecutar la suite de pruebas

Con las dos clases listas, defines tus casos como una lista de diccionarios y llamas a run_suite. Cada endpoint se prueba tres veces: válido, inválido y ausente.

tester = EndpointTester("YOUR_API_KEY")

configs = [
    {
        "name": "Contact form submission",
        "url": "https://example.com/api/contact",
        "captcha_type": "recaptcha_v2",
        "sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        "pageurl": "https://example.com/contact",
        "captcha_field": "g-recaptcha-response",
        "payload": {
            "name": "Test User",
            "email": "test@example.com",
            "message": "Automated test message",
        },
        "expected_status": 200,
        "expected_contains": "success",
    },
    {
        "name": "Newsletter signup",
        "url": "https://example.com/api/subscribe",
        "captcha_type": "turnstile",
        "sitekey": "0x4AAAA...",
        "pageurl": "https://example.com/newsletter",
        "captcha_field": "cf-turnstile-response",
        "payload": {
            "email": "test@example.com",
        },
        "expected_status": 200,
    },
]

report = tester.run_suite(configs)
print(report)

Salida:

Endpoint Tests: 5/6 passed
==================================================
  [PASS] Contact form submission (18.5s)
  [PASS] Contact form submission (invalid token) (0.3s)
  [PASS] Contact form submission (missing token) (0.2s)
  [PASS] Newsletter signup (14.2s)
  [FAIL] Newsletter signup (invalid token) (0.3s)
         Error: Endpoint accepted invalid CAPTCHA token
  [PASS] Newsletter signup (missing token) (0.2s)

El informe deja claro qué caso falló y por qué. Aquí el endpoint de la newsletter aceptó un token inválido: el tipo de fallo de seguridad que estas pruebas existen para detectar antes de producción.


Los tres casos que no deberías saltarte

Un CAPTCHA solo protege si el backend lo valida de verdad. Por eso las pruebas más valiosas no son las del camino feliz, sino las que intentan romperlo:

  • Token válido: confirma que un envío legítimo pasa y devuelve el 200 esperado.
  • Token inválido: envía una cadena falsa y verifica que el servidor la rechaza. Si la acepta, la validación es decorativa.
  • Token ausente: omite el campo. El endpoint debería responder con un 4xx o un mensaje de CAPTCHA, no procesar la solicitud.

Añade un cuarto eje para validar el límite de solicitudes: repite el envío subiendo la frecuencia y anota cuándo el endpoint empieza a devolver 429. Así confirmas que el rate limiting protege el formulario sin castigar el tráfico normal.


Coste previsible para un pipeline de QA

CaptchaAI factura por thread concurrente, no por resolución, así que el gasto es fácil de predecir. Cada thread incluye CAPTCHA ilimitados durante el mes; eliges el plan según las resoluciones que necesites en paralelo:

  • BASIC ($15/mes, 5 threads): de sobra para una suite que corre unas cuantas veces al día.
  • STANDARD ($30/mes, 15 threads): margen holgado para un pipeline con varios jobs.
  • ADVANCE ($90/mes, 50 threads): para pruebas de carga que disparan cientos de resoluciones a la vez.

Subir de plan da más threads sin tocar una línea de código.


Solución de problemas

Problema Causa Solución
Se rechaza un token válido El token caducó antes de enviarlo Reduce el tiempo entre resolver y enviar
Se acepta un token inválido El backend no valida el CAPTCHA Repórtalo como bug: es un fallo de seguridad
Todas las solicitudes devuelven 403 Faltan cookies de sesión o el token CSRF Añade las cookies de sesión o la cabecera CSRF
El endpoint JSON rechaza los datos del formulario Content-Type incorrecto Activa json_body: True en la configuración

Preguntas frecuentes

¿Qué tipos de CAPTCHA puedo cubrir con estas pruebas?

Los tres más habituales en formularios: reCAPTCHA v2, reCAPTCHA v3 y Cloudflare Turnstile. CaptchaAI también resuelve GeeTest v3, Cloudflare Challenge e imágenes tipo OCR, así que puedes ampliar el patrón a otros endpoints.

¿Cuánto tarda en llegar el token y cómo afecta a mi suite?

Depende del tipo. Un reCAPTCHA v2 suele resolverse en unos segundos y v3 tarda algo más. Si la latencia domina el tiempo total, ejecuta las resoluciones en paralelo repartiéndolas entre varios threads.

¿Necesito Selenium o un navegador headless para esto?

No, y ese es el punto. Estas pruebas envían el token directamente al endpoint con requests, sin arrancar Chrome. Reserva Selenium para las pruebas de interfaz donde necesites renderizar la página y hacer clic en el widget.

¿Puedo ejecutar estas pruebas en GitHub Actions o GitLab CI?

Sí. Guarda tu API key como secreto del pipeline, inyéctala como variable de entorno y ejecuta la suite como un paso más. Al no depender de un navegador, corre igual en un runner headless que en local.


Guías relacionadas


Lleva tus pruebas de QA al siguiente nivel: empieza con CaptchaAI y valida cada endpoint protegido con CAPTCHA.

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