Un token reCAPTCHA résolu sans erreur mais refusé par le serveur cible a presque toujours la même cause : le nom d'hôte inscrit dans le token ne correspond pas à celui de la page où vous l'envoyez. Le pageurl transmis au solveur est donc la première chose à vérifier, avant les proxys, les en-têtes ou la clé du site.
Cette panne coûte cher parce qu'elle est muette : ni code d'erreur, ni quota dépassé. La résolution réussit, siteverify renvoie un hostname inattendu, la soumission échoue. Ce guide part du symptôme, remonte à la règle reCAPTCHA en cause et donne le correctif.
Diagnostic express : du symptôme à la cause
Situez votre panne dans ce tableau, puis lisez la section correspondante.
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Token systématiquement refusé | pageurl différent du domaine de soumission |
Alignez le pageurl sur le domaine cible |
| Marche en www, échoue sans www | Variantes de domaine non équivalentes | Utilisez la variante réellement servie |
| Marche une fois sur deux | CDN ou répartiteur servant plusieurs domaines | Figez l'URL de la chaîne de redirection |
| Marche dans le navigateur, échoue dans le script | Origines différentes | Reprenez l'URL finale du navigateur |
| Token reCAPTCHA Enterprise refusé | Mauvaise liaison projet / domaine | Corrigez les domaines dans la console Enterprise |
Ce que reCAPTCHA vérifie réellement
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 }
Ce parcours tient en trois contrôles, et un seul décide du sort de votre token.
- Côté client : le widget ne se charge que sur les domaines autorisés — contrôle facultatif, désactivable par le propriétaire du site.
- Génération du token : le nom d'hôte de la page est intégré au token.
- Validation serveur :
siteverifyrenvoie ce nom d'hôte, et le code du site cible décide de l'accepter ou non.
Retenez le troisième : c'est le site, et non Google, qui tranche sur un nom d'hôte voisin. Deux sites configurés à l'identique réagissent différemment au même token.
Les trois erreurs de domaine que vous rencontrerez
Erreur 1 : nom d'hôte inattendu dans la réponse siteverify
{
"success": true,
"hostname": "subdomain.example.com",
"challenge_ts": "2025-01-15T10:30:00Z"
}
Le token est valide, mais le champ hostname ne correspond pas à celui attendu. Beaucoup d'implémentations refusent alors la requête sans message :
# 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
Trois situations le produisent.
| Nom d'hôte du token | Nom d'hôte attendu |
|---|---|
www.example.com |
example.com |
staging.example.com |
example.com |
| Hôte réécrit par un proxy ou un CDN | Domaine public |
Correctif : alignez le pageurl de votre requête de résolution sur le domaine où le token sera soumis.
Erreur 2 : le widget refuse de s'afficher
Le widget ne se charge pas et la console affiche :
ERROR: Invalid domain for site key
- Les domaines autorisés de la clé du site n'incluent pas la page courante.
- La page est chargée depuis
localhostou viafile://. - Une adresse IP remplace le nom de domaine.
Correctif côté automatisation : cette configuration appartient au propriétaire du site ; transmettez un pageurl pointant vers un domaine autorisé.
Erreur 3 : token refusé malgré une résolution correcte
{
"success": false,
"error-codes": ["invalid-input-response"]
}
Le token a été généré pour un autre domaine que celui qui le valide : pageurl sur le domaine apex, soumission sur un sous-domaine applicatif.
# 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://example.com/login", # ← Must match actual domain
"json": 1,
})
# But submitting token to:
requests.post("https://app.example.com/login", ...) # Different subdomain!
Règles de correspondance de domaine
Correspondance exacte ou caractère générique
reCAPTCHA n'impose pas par défaut une correspondance stricte de sous-domaine : tout dépend de la configuration du site.
| Domaine enregistré | Origines acceptées |
|---|---|
example.com |
example.com, www.example.com, sub.example.com (si le générique est activé) |
www.example.com |
www.example.com uniquement (en mode strict) |
*.example.com |
Tout sous-domaine de example.com |
localhost |
localhost uniquement (développement) |
Comment le serveur interprète le nom d'hôte
Dans la réponse siteverify, le hostname reflète la page qui a généré le token. Le serveur choisit ensuite entre validation permissive et validation stricte :
# 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
Une validation stricte ne pardonne aucun écart, pas même le préfixe www : partez de cette hypothèse, elle vous évite les pannes intermittentes.
Corriger les erreurs de domaine dans vos scripts
Dans l'ordre, avant de toucher au reste de la pile :
- Suivez les redirections de l'URL cible jusqu'au domaine final.
- Comparez ce domaine avec le
pageurlenvoyé au solveur. - Testez séparément les variantes www et non-www.
- Journalisez le
hostnamerenvoyé parsiteverifyà chaque résolution.
Correctif 1 : aligner le pageurl sur la cible réelle
Le correctif qui résout la majorité des cas : le pageurl désigne la page exacte où le token partira.
# Correct: pageurl matches where you'll submit the token
target_url = "https://www.example.com/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,
})
Correctif 2 : traiter www et non-www comme deux domaines
Ne devinez pas la variante servie : la redirection vous la donne.
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://example.com/login", allow_redirects=True)
actual_url = response.url # May be https://www.example.com/login after redirect
Correctif 3 : suivre la chaîne de redirection
Beaucoup de portails redirigent vers un domaine d'authentification distinct.
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://example.com/login → https://auth.example.com/login
final_url = get_final_url("https://example.com/login")
# Use final_url as pageurl for solver
Correctif 4 : lire le domaine dans l'iframe reCAPTCHA
Quand le widget est chargé dans une iframe, le domaine de liaison se lit dans le HTML.
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 script de diagnostic à garder sous la main
Avant d'ouvrir un ticket, comparez l'URL visée et celle que le site sert réellement.
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://example.com/login")
diag.report()
Journalisez ce nom d'hôte à chaque résolution : vous saurez si une régression vient d'un changement côté site ou de votre code. Côté RGPD, limitez ces logs à l'URL et à l'horodatage.
Cas concret : un SaaS français derrière un CDN
Une équipe QA parisienne teste le parcours de connexion d'une application hébergée chez OVHcloud. Le domaine public est www.exemple.fr, la page de connexion redirige vers auth.exemple.fr, et le CDN sert la variante apex exemple.fr sur certaines régions. Les scripts envoient https://exemple.fr/connexion comme pageurl : en staging tout passe, en production un test sur trois échoue. Les logs le montrent — le hostname renvoyé par siteverify alterne entre exemple.fr et auth.exemple.fr, et le back-end valide en mode strict.
La correction tient en deux gestes : suivre la redirection avant chaque résolution, puis passer l'URL finale en pageurl. Le gain dépasse la fiabilité : les plans CaptchaAI se comptent en threads simultanés — BASIC ($15/mois, 5 threads) — et non en résolutions, donc chaque retry inutile mobilise un thread que vos tests n'ont plus. Cloudflare Turnstile et GeeTest v3 lient eux aussi le token à l'origine de la page.
Questions fréquentes
Comment savoir quel nom d'hôte est inscrit dans mon token ?
Validez-le une fois avec siteverify en environnement de test et lisez le champ hostname. C'est la seule source fiable : le token est opaque, et déduire le domaine de l'URL de départ est justement ce qui crée la panne.
Un token généré pour un domaine fonctionne-t-il sur un autre ?
Non. Le token est lié au nom d'hôte de la page qui l'a produit : example.com et other-site.com sont incompatibles, et deux sous-domaines échouent dès que la validation est stricte.
Un reverse proxy ou Cloudflare peut-il fausser la vérification ?
Oui, dès que la couche intermédiaire réécrit l'hôte ou sert plusieurs domaines. Deux vérifications suffisent à trancher :
- l'en-tête
Hostréellement émis par votre script ; - le nom d'hôte final vu dans la barre d'adresse du navigateur.
Mes tentatives échouées consomment-elles mes threads CaptchaAI ?
Indirectement, oui : chaque requête occupe un thread le temps de la résolution, et un pageurl erroné consomme ce temps pour rien. La facturation reste au thread simultané, avec des résolutions illimitées par thread, mais votre débit utile se dégrade.
Faut-il une clé du site différente entre staging et production ?
Pas nécessairement, mais les domaines des deux environnements doivent figurer dans la clé utilisée, sinon localhost ou staging.example.com échouera. Vérifiez cette liste avant d'accuser le solveur.
À retenir
- Le token est lié au nom d'hôte de la page qui l'a généré : l'écart vient presque toujours du
pageurltransmis à CaptchaAI. - Suivez les redirections pour identifier le domaine réellement servi, et traitez www et non-www comme deux domaines.
- Journalisez le
hostnamerenvoyé parsiteverifypour repérer une dérive avant qu'elle ne casse vos scripts.