¿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.