La respuesta corta: no elijas proveedor al escribir el código, detéctalo en cada carga. Un dominio puede servir reCAPTCHA v2 en /login y Cloudflare Turnstile en /checkout, y basta una prueba A/B del equipo de seguridad para que tu script falle sin que tú hayas tocado nada.
El fallo es silencioso: el envío a la API funciona y el thread se consume, pero el formulario rechaza el token porque fue al campo del otro proveedor. Son dos problemas: identificar el widget y enviarlo con su method.
Cómo distinguir reCAPTCHA v2 de Turnstile en el HTML
De cada proveedor necesitas tres datos: la clase CSS del contenedor, el script que carga el widget y el campo del token.
| Proveedor | Marcador HTML | URL del script | Campo de respuesta |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
La fila de hCaptcha está para que sepas reconocerlo: CaptchaAI no resuelve hCaptcha ni FunCaptcha (Arkose Labs). Sí cubre reCAPTCHA v2 —invisible, callback y Enterprise—, reCAPTCHA v3, Turnstile y Cloudflare Challenge, GeeTest v3, imagen/OCR, grid y BLS; CaptchaFox, Friendly Captcha y Lemin están en beta.
Regla práctica: busca primero la clase del proveedor y solo después el data-sitekey. Al revés, cualquier widget con ese atributo da un falso positivo y acabas enviando un sitekey de Turnstile con method=userrecaptcha.
Por qué un mismo dominio mezcla proveedores
Casi nunca es descuido; suele haber una razón operativa.
| Escenario | Cómo se manifiesta |
|---|---|
| Páginas distintas, proveedores distintos | Login = reCAPTCHA, checkout = Turnstile |
| Prueba A/B entre proveedores | La misma página muestra un tipo u otro al azar |
| Migración en curso | Las páginas antiguas llevan reCAPTCHA, las nuevas llevan Turnstile |
| Repliegue ante fallo | El proveedor principal no responde y entra el secundario |
| Variación por región | reCAPTCHA para visitantes de EE. UU., Turnstile para la UE (GDPR) |
La variación por región es la que más despista. Una tienda con presencia en España y México puede servir Turnstile al tráfico europeo —por el encaje con el RGPD y la LOPDGDD suelen preferir un proveedor que no envíe datos a Google— y dejar reCAPTCHA en el resto. Si tu QA corre desde Fráncfort y la de un compañero desde São Paulo, cada uno verá un widget distinto en la misma URL.
Python: detección automática y resolución
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://staging.example.com/qa-login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
El script descarga la página, aplica los patrones y devuelve el proveedor, el method, el sitekey y el campo de destino del token. La resolución es una sola función: no hay dos rutas que mantener, solo un mapeo distinto.
JavaScript: detección dinámica del widget
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://staging.example.com/qa-login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
La versión en JavaScript añade un tercer intento para reCAPTCHA renderizado por script, cuando el HTML no trae el div con la clase. Es habitual en aplicaciones de una sola página: el marcador aparece tras ejecutarse el bundle.
Errores frecuentes y cómo corregirlos
| Síntoma | Causa probable | Corrección |
|---|---|---|
| Se detecta el tipo de CAPTCHA equivocado | El patrón coincide con otro elemento | Comprueba primero la clase del proveedor, no solo data-sitekey |
| El token se rechaza pese a resolverse bien | Campo de respuesta incorrecto | Empareja el campo con el proveedor: g-recaptcha-response frente a cf-turnstile-response |
| El tipo cambia entre una visita y otra | Prueba A/B o selección por geografía | Detecta en tiempo de ejecución; nunca fijes el proveedor en el código |
| Se detectan los dos proveedores | Uno está oculto o inactivo | Comprueba la visibilidad y resuelve solo el CAPTCHA visible |
| Falla en widgets renderizados por script | No hay marcador en el HTML de origen | Busca grecaptcha.render() o turnstile.render() en los scripts |
Hay un sexto caso que no es error de código: el sitekey rota. Si la detección funciona pero un dominio rechaza todos los tokens, relee el HTML y compara el sitekey con el que guardaste. Cachear el sitekey junto al proveedor es la trampa más común aquí.
Qué implica para tu plan y tus threads
Detectar dos proveedores no significa pagar dos veces. CaptchaAI factura por thread concurrente —una tarea en vuelo—, no por resolución: reCAPTCHA v2 y Turnstile consumen la misma capacidad, sin recargos por tipo y con resoluciones ilimitadas al mes.
BASIC ($15/mes, 5 threads) cubre la QA de un checkout pequeño o el trabajo de un freelance; STANDARD ($30/mes, 15 threads) y ADVANCE ($90/mes, 50 threads) son los tramos de una agencia con varios dominios. Por encima: PREMIUM ($170/mes, 100 threads), CORPORATE ($240/mes, 150 threads), ENTERPRISE ($300/mes, 200 threads), VIP-1 ($1,500/mes, 1.000 threads), VIP-2 ($4,500/mes, 3.000 threads) y VIP-3 ($7,500/mes, 5.000 threads).
En equipos que facturan en pesos o soles, lo que decide la compra no es el precio sino el costo mensual fijo en USD: con threads sabes lo que gastas aunque el sitio sirva el doble de desafíos. Con pago por resolución, una prueba A/B ajena te cambia la factura.
En portales públicos —trámites del SAT, citas previas españolas, portales BLS de visado— el volumen es irregular: picos cortos y horas planas. Ahí rinde más un plan pequeño con reintentos espaciados que threads ociosos. Respeta los términos de servicio y la normativa de protección de datos aplicable.
Preguntas frecuentes
¿Necesito dos claves API para resolver reCAPTCHA v2 y Turnstile?
No. Una sola clave API cubre ambos. Solo cambian el valor de method y el nombre del parámetro del sitekey: googlekey para reCAPTCHA, sitekey para Turnstile.
¿Puedo cachear el proveedor detectado para ahorrar una petición?
Solo dentro de una sesión y asumiendo el riesgo. Con pruebas A/B o selección por región, la caché te dará el widget equivocado en cuanto rote. Volver a detectar cuesta una lectura del HTML que ya descargas.
¿Sirve este patrón para GeeTest v3 o Cloudflare Challenge?
Sí: añades un patrón más y una rama en el mapeo. GeeTest v3 y Cloudflare Challenge son compatibles. GeeTest v4 todavía no está disponible; figura como próximamente.
¿Qué hago si el sitio muestra hCaptcha en alguna página?
Reconócelo por la clase h-captcha y trátalo aparte: CaptchaAI no resuelve hCaptcha por ahora. Que la detección lo identifique y devuelva un estado explícito, en vez de enviarlo como reCAPTCHA y quemar reintentos en balde.
¿Cambia el tiempo de resolución al alternar entre los dos tipos?
Cada tipo mantiene su propio tiempo de resolución y ninguno penaliza al otro: los threads son genéricos. Ajusta el sondeo para no consultar res.php antes de que la tarea pueda completarse.
Artículos relacionados
- Resolver el callback de reCAPTCHA v2 con la API
- Distinguir Cloudflare Challenge de Turnstile
- GeeTest frente a Cloudflare Turnstile
Próximos pasos
Deja de mantener dos rutas de código: obtén tu clave API de CaptchaAI y monta la detección una sola vez.
Guías relacionadas: