Troubleshooting

Erros e correções de verificação de domínio reCAPTCHA

O token do reCAPTCHA nasce carimbado com o hostname da página que o gerou, e é esse carimbo, não a chave, que o servidor confere ao aceitar o envio. Todo erro de verificação de domínio se resume a isso: o domínio declarado no pageurl não é o domínio de onde o token saiu. Com staging.example.com no pageurl e o POST indo para www.staging.example.com, a verificação falha em silêncio — nenhuma exceção é lançada, nenhum log óbvio, só um formulário que não passa.

Abaixo estão os três erros de domínio que aparecem de verdade em automação, na ordem em que compensa checá-los, com a correção de cada um em Python contra staging.example.com, em QA autorizado na sua própria aplicação.

Duas checagens já resolvem boa parte dos chamados:

  • O pageurl é a URL final que o navegador mostra, depois dos redirecionamentos?
  • O POST sai do mesmo domínio da resolução? Em validação estrita, example.com e www.example.com são domínios diferentes.

Do sintoma à causa: triagem dos erros de domínio

Sintoma Causa provável Correção
Token sempre recusado pageurl diferente do domínio de destino Alinhe o pageurl à URL do POST
Funciona com www, falha sem www Variante de domínio errada Fixe a variante que o site usa e siga os redirecionamentos
Falha intermitente CDN ou balanceador responde por domínios distintos Use sempre a URL final da cadeia
Funciona no navegador, falha no script O script parte de outra origem Copie a URL final da barra de endereço
Token Enterprise recusado Projeto ou vínculo de domínio errado Revise os domínios no console do Enterprise

Como o token fica preso ao domínio de origem

O vínculo nasce quando o widget gera o token e é conferido de novo no siteverify:

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 }

Os três pontos em que o domínio é conferido

Ponto de verificação O que é conferido
No navegador O widget só carrega em domínios permitidos (opcional — o dono do site pode desativar)
Na geração do token O hostname gravado no token é o da origem da página
Na validação do servidor O siteverify devolve o hostname; cabe ao servidor decidir se ele serve

O terceiro ponto é o que surpreende: o siteverify pode responder "success": true e a aplicação recusar mesmo assim, porque a comparação roda no código do dono do site.

Correspondência exata, curinga e a armadilha do www

A verificação não é, por padrão, estrita quanto a subdomínio: depende de como a sitekey (chave pública do widget) foi registrada.

Domínio registrado Origens aceitas
example.com example.com, www.example.com, sub.example.com (com curinga habilitado)
www.example.com Somente www.example.com (em modo estrito)
*.example.com Qualquer subdomínio de example.com
localhost Apenas localhost, para desenvolvimento

A linha do localhost explica metade dos "na minha máquina funciona".

Erro 1: o siteverify devolve um hostname diferente

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

O token é válido, mas o hostname mostra um domínio que a aplicação não esperava — e uma validação como esta recusa o envio:

# 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

Causas típicas:

  • Token resolvido para www.example.com e validado em example.com
  • Token resolvido em staging.example.com e validado no domínio raiz
  • Proxy ou CDN na frente da aplicação altera o hostname aparente

Correção: o pageurl precisa ser exatamente o domínio de onde o token sairá. "Quase igual" não conta.

Erro 2: o widget se recusa a carregar

Aqui o erro aparece antes de existir token: o widget não renderiza e o console mostra:

ERROR: Invalid domain for site key

Causas típicas:

  • Os domínios permitidos da sitekey não incluem o domínio da página aberta
  • A página é servida a partir de localhost ou pelo protocolo file://
  • O acesso usa endereço IP em vez de nome de domínio

Isso é configuração do dono do site: na automação, cabe a você enviar um pageurl que corresponda a um domínio permitido.

Erro 3: token recusado mesmo com a resolução correta

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

O token chegou inteiro e ainda assim o siteverify responde invalid-input-response: ele nasceu em outro domínio. Em automação a causa é quase sempre um pageurl que não é o destino real do POST:

# 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!

Um subdomínio de diferença basta — barato de cometer, caro de achar no log.

Como o servidor decide aceitar o hostname

Convivem duas políticas de validação: a permissiva aceita qualquer subdomínio da base; a estrita, só a correspondência exata.

# 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

Você não vê o código do outro lado, mas dá para inferir: se o token gerado em www. passa e o do domínio raiz não, a validação é estrita.

Quatro correções para a automação

Correção 1: use como pageurl a URL final

Encerra a maioria dos chamados:

# 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,
})

Correção 2: decida entre www e não www antes de resolver

Descubra qual variante o site usa e padronize o fluxo inteiro nela:

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

Correção 3: siga a cadeia de redirecionamentos

Um login iniciado em staging.example.com pode terminar em auth.staging.example.com.

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

Correção 4: leia o domínio direto do widget

Quando a página monta o widget dinamicamente, o iframe do reCAPTCHA é a referência; sem o parâmetro de domínio, use o hostname da página.

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

Um diagnóstico antes de gastar créditos

Rodar esta classe uma vez por alvo custa menos que descobrir a divergência depois de centenas de resoluções. Ela segue os redirecionamentos, testa a variante de www e devolve o pageurl a usar:

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

Cenário: staging duplicado atrás de um CDN

Situação corriqueira em times brasileiros: o staging responde em staging.example.com e em www.staging.example.com, com um CDN na frente. Na máquina do desenvolvedor a suíte passa, porque o navegador guardou o redirecionamento em cache; no CI, o worker sobe em sa-east-1, faz a requisição limpa, cai na variante www e todos os tokens são recusados de uma vez.

O log só resolve o caso quando registra as duas pontas: a URL usada como pageurl e o domínio do POST. E, por ser ambiente de teste, use dados fictícios no formulário — as obrigações da LGPD também alcançam o staging.

Perguntas frequentes

Como sei se o erro é da minha automação ou da configuração do site?

Abra a página no navegador. Se o widget carrega, o domínio está liberado e o problema está no pageurl da sua requisição; se falha ali, a restrição está na sitekey e só o dono do site corrige.

Trocar a sitekey resolve um erro de domínio?

Não. A sitekey diz qual widget deve ser resolvido; o vínculo de domínio vem do pageurl e da lista configurada pelo dono do site. Trocar a chave sem alinhar a URL só substitui um erro por outro.

Posso resolver em staging e reaproveitar o token em produção?

Não. O token carrega o hostname da geração, e os dois ambientes são domínios distintos. Cada um precisa da sua própria resolução, com o pageurl daquele ambiente.

Preciso de um plano grande para validar essas correções?

Não. O BASIC (US$ 15/mês, 5 threads) cobre a validação de um fluxo em staging: a cobrança é por thread simultânea, com resoluções ilimitadas dentro do plano.

Resolva o reCAPTCHA com o domínio certo

Verificação de domínio é uma comparação de hostname: o token vale onde nasceu. A falha mais comum continua sendo o pageurl que não corresponde ao destino do envio — e a CaptchaAI usa exatamente a URL que você informa. Siga os redirecionamentos, fixe a variante de www e registre a URL final em log antes de aumentar o volume.

Guias relacionados

Os comentários estão desativados para este artigo.