Solución de Problemas

Errores y correcciones de verificación de dominio reCAPTCHA

Si tu integración resuelve el CAPTCHA sin errores y aun así el servidor responde que el token no es válido, el problema casi nunca es la resolución: es el dominio. Cada token de reCAPTCHA lleva incrustado el hostname de la página donde se generó, y basta con que no sea el que valida el backend para que todo falle en silencio, sin excepción ni log claro.

La corrección suele caber en una línea: el pageurl que envías al servicio de resolución tiene que ser la URL final donde ese token se va a consumir. El resto de esta guía es cómo llegar a esa URL y cómo distinguir este fallo de otros que se le parecen.


Dónde se rompe realmente el dominio

Site owner registers reCAPTCHA → adds allowed domains (example.com, www.example.com)
    ↓
reCAPTCHA widget loads on example.com → matches allowed domain ✓
    ↓
Token generated with embedded hostname
    ↓
Server validates token via siteverify API
    ↓
Google checks: Does token hostname match allowed domains?
    ├─ YES → { "success": true, "hostname": "example.com" }
    └─ NO  → { "success": false, error or hostname mismatch }

Esa cadena tiene tres puntos de control y el síntoma es idéntico en los tres:

  • Lado del cliente: el widget solo se carga en dominios permitidos (opcional; el propietario puede desactivarlo).
  • Generación del token: el hostname incrustado es el del origen de la página.
  • Validación en el servidor: siteverify devuelve el hostname y el backend decide si lo acepta.

Los dos primeros dependen del propietario del sitio; el tercero es código tuyo.


Los tres errores de dominio que verás en producción

Error 1: el hostname no coincide en la respuesta de siteverify

{
    "success": true,
    "hostname": "subdomain.example.com",
    "challenge_ts": "2025-01-15T10:30:00Z"
}

Fíjate en el detalle incómodo: success es true. El token es legítimo, pero el hostname no es el que el backend espera, y esa comparación posterior es la que falla:

# Server-side validation that checks hostname
def validate_token(token, secret_key, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret_key, "response": token},
    ).json()

    if not result.get("success"):
        return False

    # This check causes failures when hostnames don't match
    if result.get("hostname") != expected_hostname:
        return False  # Domain mismatch!

    return True

Origen habitual: el token se resolvió para www.example.com o staging.example.com y se valida contra example.com, o bien un proxy o CDN altera el hostname aparente.

Corrección: haz que el pageurl sea exactamente el dominio donde vas a enviar el token.

Error 2: el widget se niega a cargar

El widget no aparece o devuelve un mensaje seco:

ERROR: Invalid domain for site key

Origen habitual: los dominios del sitekey no incluyen el de la página actual, cargas el widget desde localhost o file://, o usas una IP en lugar de un nombre de dominio.

Corrección en automatización: es configuración del propietario, no algo que tu script fuerce. Lo que controlas es enviar un pageurl de un dominio permitido.

Error 3: token rechazado pese a una resolución correcta

{
    "success": false,
    "error-codes": ["invalid-input-response"]
}

El token se generó para un dominio distinto del que se valida. Es el clásico de la automatización: el pageurl enviado al solver no es el destino real.

# WRONG: pageurl doesn't match actual target
submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": "https://staging.example.com/qa-login",  # ← Must match actual domain
    "json": 1,
})

# But submitting token to:
requests.post("https://app.staging.example.com/qa-login", ...)  # Different subdomain!

Un subdominio de diferencia basta.

Del síntoma a la causa

Síntoma Causa probable Cómo lo confirmas Qué cambias
El token se rechaza siempre El pageurl no coincide con el dominio de destino Compara el pageurl enviado con el dominio donde se envía el token Actualiza el pageurl para que coincida
Funciona con www y falla sin www (o al revés) Variante de dominio distinta Revisa el comportamiento de redirección del sitio Usa la variante que el propio sitio impone
Unas veces funciona y otras no Una CDN o un balanceador sirven dominios distintos Comprueba si el hostname varía entre solicitudes Fija una URL coherente al final de la cadena
Funciona en el navegador y falla en el script El script parte de un origen distinto Compara la barra de direcciones del navegador con la URL del script Alinea el script con la URL final del navegador
Token Enterprise rechazado Proyecto o dominio mal enlazado Verifica que el sitekey Enterprise corresponda al dominio Corrige la configuración de dominio en la consola Enterprise

Reglas de coincidencia: qué acepta realmente reCAPTCHA

Detrás de casi todos estos fallos hay una suposición equivocada: reCAPTCHA no aplica por defecto coincidencia estricta de subdominios. Depende de cómo registró los dominios el propietario.

Dominio registrado Orígenes que se aceptan
example.com example.com, www.example.com, sub.example.com (si el comodín está habilitado)
www.example.com Solo www.example.com (en configuración estricta)
*.example.com Cualquier subdominio de example.com
localhost Solo localhost (entorno de desarrollo)

Desde fuera no ves cuál de estas configuraciones usa el sitio: solo la deduces por el hostname que devuelve siteverify.

Cómo se comporta el hostname en el servidor

El hostname de siteverify refleja la página donde se generó el token. Quien decide si vale es el backend, y ahí hay dos escuelas:

# Permissive validation (accepts any subdomain)
def validate_permissive(token, secret, base_domain):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    if not result.get("success"):
        return False

    hostname = result.get("hostname", "")
    return hostname == base_domain or hostname.endswith(f".{base_domain}")


# Strict validation (exact match only)
def validate_strict(token, secret, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    return result.get("success") and result.get("hostname") == expected_hostname

Con la variante estricta, un token bien resuelto para www. se rechaza en el dominio desnudo. No hay nada que arreglar en el solver: hay que alinear la URL de origen.


Escenario típico: portales públicos con redirección

Los portales públicos de habla hispana —cita previa en España, trámites tipo SAT en México, AFIP en Argentina— son el caso que más veces provoca esto:

  • Copias la URL del portal de entrada y la usas como pageurl.
  • El portal te manda a un dominio de autenticación antes de enseñar el formulario.
  • El token se resuelve bien, pero nace atado al hostname equivocado.

Respeta los términos de servicio del portal y la normativa de protección de datos aplicable.


Cuatro correcciones concretas

En orden: la primera resuelve la mayoría de los casos y cada siguiente cubre un supuesto más raro.

Corrección 1: haz coincidir el pageurl con el destino exacto

# Correct: pageurl matches where you'll submit the token
target_url = "https://www.staging.example.com/qa-login"

submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    "pageurl": target_url,  # Must match the actual domain
    "json": 1,
})

Corrección 2: decide la variante www antes de resolver

No adivines cuál usa el sitio. Pídela y deja que redirija:

from urllib.parse import urlparse

def normalize_url(url):
    """Normalize URL for consistent domain matching."""
    parsed = urlparse(url)
    # Use exactly what the target site uses
    # Check if the site redirects www → non-www or vice versa
    return f"{parsed.scheme}://{parsed.netloc}{parsed.path}"

# Test which variant the site uses
response = requests.get("https://staging.example.com/qa-login", allow_redirects=True)
actual_url = response.url  # May be https://www.staging.example.com/qa-login after redirect

Corrección 3: sigue la cadena de redirecciones hasta el final

def get_final_url(url):
    """Follow redirects to find the actual CAPTCHA page domain."""
    response = requests.get(url, allow_redirects=True, timeout=15)
    return response.url

# Login URL might redirect:
# https://staging.example.com/qa-login → https://auth.staging.example.com/qa-login
final_url = get_final_url("https://staging.example.com/qa-login")
# Use final_url as pageurl for solver

Corrección 4: lee el dominio desde el propio widget

Último recurso cuando la página monta el widget por JavaScript:

from bs4 import BeautifulSoup
from urllib.parse import urlparse

def extract_recaptcha_domain(html, page_url):
    """Extract the domain reCAPTCHA uses for token binding."""
    soup = BeautifulSoup(html, "html.parser")

    # Check for reCAPTCHA iframe
    iframe = soup.find("iframe", src=lambda s: s and "recaptcha" in s)
    if iframe:
        src = iframe.get("src", "")
        # The iframe URL may contain the domain parameter
        if "domain=" in src:
            # Extract domain from iframe URL
            pass

    # Default: use the page URL's domain
    return urlparse(page_url).netloc

Un diagnóstico automatizado antes de gastar threads

Antes de mandar nada al solver conviene una comprobación barata que te diga qué URL usar. Este script sigue redirecciones, contrasta la variante con y sin www e imprime el pageurl recomendado:

import requests
from urllib.parse import urlparse

class DomainDiagnostic:
    """Diagnose domain verification issues for reCAPTCHA solving."""

    def __init__(self, target_url):
        self.target_url = target_url
        self.issues = []

    def check_redirects(self):
        """Check if the URL redirects to a different domain."""
        try:
            response = requests.get(
                self.target_url, allow_redirects=True, timeout=15,
                headers={"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0"},
            )
            final_url = response.url
            original_domain = urlparse(self.target_url).netloc
            final_domain = urlparse(final_url).netloc

            if original_domain != final_domain:
                self.issues.append({
                    "type": "redirect",
                    "message": f"Redirects from {original_domain} to {final_domain}",
                    "fix": f"Use pageurl: {final_url}",
                })

            return final_url
        except Exception as e:
            self.issues.append({"type": "error", "message": str(e)})
            return self.target_url

    def check_www_variant(self):
        """Check if www and non-www point to the same content."""
        parsed = urlparse(self.target_url)
        domain = parsed.netloc

        if domain.startswith("www."):
            alt_domain = domain[4:]
        else:
            alt_domain = f"www.{domain}"

        alt_url = self.target_url.replace(domain, alt_domain)

        try:
            alt_response = requests.get(alt_url, allow_redirects=True, timeout=10)
            alt_final = urlparse(alt_response.url).netloc

            if alt_final != domain and alt_final != alt_domain:
                self.issues.append({
                    "type": "www_redirect",
                    "message": f"{alt_domain} redirects to {alt_final}",
                })
        except Exception:
            pass

    def report(self):
        """Generate diagnostic report."""
        final_url = self.check_redirects()
        self.check_www_variant()

        print(f"Target URL: {self.target_url}")
        print(f"Final URL:  {final_url}")
        print(f"Use as pageurl: {final_url}")

        if self.issues:
            print("\nIssues found:")
            for issue in self.issues:
                print(f"  [{issue['type']}] {issue['message']}")
                if "fix" in issue:
                    print(f"  Fix: {issue['fix']}")
        else:
            print("\nNo domain issues detected.")


# Usage
diag = DomainDiagnostic("https://staging.example.com/qa-login")
diag.report()

Ejecútalo una vez por entorno: con el plan BASIC ($15/mes, 5 threads), cada thread ocupado por un token que acabará rechazado es capacidad perdida.


Preguntas frecuentes

¿Cómo sé si el fallo es de dominio y no del sitekey?

Te lo dice la respuesta de siteverify:

  • success: true y falla tu comparación de hostname → problema de dominio.
  • invalid-input-response con success: false → revisa primero el pageurl y solo después el sitekey.

¿Tengo que enviar la URL completa o basta el dominio?

Dos cosas distintas:

  • reCAPTCHA valida el hostname, no la ruta: /login y /registro dan lo mismo.
  • Aun así, envía la URL completa como pageurl: parte de la integración se apoya en ella para localizar el widget.

¿Sirve el mismo token en dos subdominios de mi propio sitio?

Depende del backend:

  • Validación permisiva (endswith): sí.
  • Validación estricta: no.
  • Entre dominios distintos: nunca. El token está atado al hostname donde nació.

¿CaptchaAI comprueba por mí que el dominio esté bien configurado?

No. CaptchaAI genera el token contra el pageurl que le pasas y no valida la configuración de dominios del destino. Esa comprobación es tuya, y la automatiza el script de diagnóstico de esta guía.

¿Esto pasa igual con Cloudflare Turnstile?

Parecido, con un matiz:

  • Igual: el token se emite para un contexto concreto y se rechaza fuera de él.
  • Distinto: cambian los mensajes de error. No traslades un diagnóstico de reCAPTCHA a Cloudflare Turnstile sin mirar qué devuelve tu validación.

Para llevarte

El token de reCAPTCHA vive atado al hostname donde se generó, y el error número uno en automatización es un pageurl que no corresponde al destino real. Tres hábitos lo evitan:

  • Sigue las redirecciones y fija la variante www antes de resolver con CaptchaAI.
  • Ejecuta el diagnóstico antes de gastar threads, no después de acumular rechazos.
  • Guarda el pageurl resuelto con la configuración del entorno, para comparar cuando el sitio cambie una redirección.

Artículos relacionados

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