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
POSTsai do mesmo domínio da resolução? Em validação estrita,example.comewww.example.comsã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.come validado emexample.com - Token resolvido em
staging.example.come 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
localhostou pelo protocolofile:// - 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.