Un endpoint protégé par un CAPTCHA reste un endpoint que votre équipe QA doit valider : statut HTTP, message d'erreur, limitation de débit, rejet des jetons invalides. Le défi CAPTCHA ne devrait pas vous empêcher d'écrire ces tests. En résolvant le CAPTCHA via l'API CaptchaAI, puis en soumettant le token directement au backend, vous testez la logique serveur sans piloter de navigateur ni cliquer sur la moindre case.
Pourquoi tester une API protégée par CAPTCHA sans navigateur
Piloter un navigateur headless pour chaque test est lent, fragile et coûteux en ressources. Dès que la seule barrière qui vous sépare de l'endpoint est le CAPTCHA, une requête HTTP directe suffit. Le tableau suivant résume ce que cette approche vous permet de valider :
| Objectif du test | Ce que la requête directe prouve |
|---|---|
| Validation côté serveur | Le backend vérifie réellement le token auprès de Google ou de Cloudflare, pas seulement sa présence dans le payload |
| Tests de charge | L'endpoint tient le volume sans la surcharge d'un navigateur à chaque itération |
| Intégration CI/CD | La soumission de formulaire passe dans GitHub Actions ou GitLab CI avec un vrai token |
| Réponses d'erreur | Un token invalide ou expiré déclenche bien un code 4xx et le message attendu |
Données de test : utilisez des adresses e-mail et des noms fictifs, et minimisez ce que vous conservez dans vos rapports. C'est la bonne hygiène RGPD, et cela évite de traîner de vraies coordonnées dans les logs d'un runner CI hébergé chez OVHcloud, Scaleway ou dans une région AWS eu-west-3 (Paris).
Le principe : résoudre, construire, soumettre, valider
Le flux tient en quatre étapes. Vous obtenez un token via l'API, vous l'insérez dans le payload du formulaire, vous envoyez la requête au backend, puis vous vérifiez la réponse.
┌──────────┐ ┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ Solve │────▶│ Build │────▶│ POST to │────▶│ Validate │
│ CAPTCHA │ │ Request │ │ Endpoint │ │ Response │
│ (API) │ │ Payload │ │ │ │ │
└──────────┘ └────────────┘ └──────────────┘ └──────────────┘
Aucun navigateur n'est nécessaire pour la plupart des tests de points de terminaison.
Mise en œuvre
Deux classes suffisent : l'une résout le CAPTCHA et renvoie un token, l'autre pilote le test et compare la réponse à ce que vous attendez. Vous les réutilisez pour tous vos endpoints.
Le fournisseur de tokens
Cette première classe isole toute la logique d'appel à l'API : envoi de la tâche à in.php, puis interrogation régulière de res.php jusqu'à obtenir le token. Elle couvre reCAPTCHA v2, reCAPTCHA v3 et Cloudflare Turnstile, qui sont les types que vous croiserez le plus souvent sur des formulaires publics.
import time
import requests
class TokenProvider:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def get_recaptcha_token(self, sitekey, pageurl, version="v2"):
params = {
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
}
if version == "v3":
params["version"] = "v3"
params["action"] = "submit"
return self._solve(params, initial_wait=15 if version == "v3" else 10)
def get_turnstile_token(self, sitekey, pageurl):
return self._solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
})
def _solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(result["request"])
raise TimeoutError("Timed out")
Le testeur d'endpoint
La seconde classe orchestre le test lui-même. Elle demande un token au fournisseur, construit le payload avec le bon nom de champ, envoie la requête et compare la réponse à ce que vous attendez. Le nom du champ dépend du type de CAPTCHA :
| Type de CAPTCHA | Paramètre method |
Champ du token |
|---|---|---|
| reCAPTCHA v2 | userrecaptcha | g-recaptcha-response |
| reCAPTCHA v3 | userrecaptcha (version=v3) | g-recaptcha-response |
| Cloudflare Turnstile | turnstile | cf-turnstile-response |
Elle expose aussi deux tests négatifs indispensables : le token invalide et le token absent.
import json
import time
class EndpointTester:
def __init__(self, api_key):
self.token_provider = TokenProvider(api_key)
self.session = requests.Session()
self.results = []
def test_endpoint(self, config):
"""
config: {
"name": "test name",
"url": "endpoint URL",
"method": "POST",
"captcha_type": "recaptcha_v2" | "recaptcha_v3" | "turnstile",
"sitekey": "...",
"pageurl": "...",
"captcha_field": "g-recaptcha-response",
"payload": { ... form data ... },
"expected_status": 200,
"expected_contains": "success",
}
"""
start = time.time()
result = {"name": config["name"], "passed": False}
try:
# Get CAPTCHA token
captcha_type = config.get("captcha_type", "recaptcha_v2")
if captcha_type == "recaptcha_v2":
token = self.token_provider.get_recaptcha_token(
config["sitekey"], config["pageurl"]
)
elif captcha_type == "recaptcha_v3":
token = self.token_provider.get_recaptcha_token(
config["sitekey"], config["pageurl"], version="v3"
)
elif captcha_type == "turnstile":
token = self.token_provider.get_turnstile_token(
config["sitekey"], config["pageurl"]
)
else:
raise ValueError(f"Unknown captcha type: {captcha_type}")
# Build payload
payload = {**config.get("payload", {})}
captcha_field = config.get("captcha_field", "g-recaptcha-response")
payload[captcha_field] = token
# Submit request
method = config.get("method", "POST").upper()
headers = config.get("headers", {})
if config.get("json_body"):
resp = self.session.request(
method, config["url"], json=payload, headers=headers
)
else:
resp = self.session.request(
method, config["url"], data=payload, headers=headers
)
# Validate response
result["status_code"] = resp.status_code
result["response_length"] = len(resp.text)
result["elapsed"] = round(time.time() - start, 2)
# Check expected status
expected_status = config.get("expected_status", 200)
if resp.status_code != expected_status:
result["error"] = f"Expected {expected_status}, got {resp.status_code}"
self.results.append(result)
return result
# Check expected content
expected = config.get("expected_contains")
if expected and expected.lower() not in resp.text.lower():
result["error"] = f"Response missing: '{expected}'"
self.results.append(result)
return result
result["passed"] = True
except Exception as e:
result["error"] = str(e)
result["elapsed"] = round(time.time() - start, 2)
self.results.append(result)
return result
def test_invalid_token(self, config):
"""Test that endpoint rejects invalid CAPTCHA tokens."""
invalid_config = {**config}
invalid_config["name"] = f"{config['name']} (invalid token)"
# Override with fake token
payload = {**config.get("payload", {})}
captcha_field = config.get("captcha_field", "g-recaptcha-response")
payload[captcha_field] = "INVALID_TOKEN_12345"
start = time.time()
result = {"name": invalid_config["name"], "passed": False}
try:
resp = self.session.post(config["url"], data=payload)
result["status_code"] = resp.status_code
result["elapsed"] = round(time.time() - start, 2)
# Should reject — 4xx or error message
if resp.status_code >= 400 or "error" in resp.text.lower() or "invalid" in resp.text.lower():
result["passed"] = True
else:
result["error"] = "Endpoint accepted invalid CAPTCHA token"
except Exception as e:
result["error"] = str(e)
result["elapsed"] = round(time.time() - start, 2)
self.results.append(result)
return result
def test_missing_token(self, config):
"""Test that endpoint rejects missing CAPTCHA token."""
start = time.time()
result = {"name": f"{config['name']} (missing token)", "passed": False}
try:
payload = config.get("payload", {})
resp = self.session.post(config["url"], data=payload)
result["status_code"] = resp.status_code
result["elapsed"] = round(time.time() - start, 2)
if resp.status_code >= 400 or "captcha" in resp.text.lower():
result["passed"] = True
else:
result["error"] = "Endpoint accepted request without CAPTCHA"
except Exception as e:
result["error"] = str(e)
result["elapsed"] = round(time.time() - start, 2)
self.results.append(result)
return result
def run_suite(self, configs):
"""Run a full test suite against multiple endpoints."""
for config in configs:
self.test_endpoint(config)
self.test_invalid_token(config)
self.test_missing_token(config)
return self.report()
def report(self):
passed = sum(1 for r in self.results if r["passed"])
total = len(self.results)
lines = [f"Endpoint Tests: {passed}/{total} passed", "=" * 50]
for r in self.results:
status = "PASS" if r["passed"] else "FAIL"
elapsed = r.get("elapsed", "?")
lines.append(f" [{status}] {r['name']} ({elapsed}s)")
if r.get("error"):
lines.append(f" Error: {r['error']}")
return "\n".join(lines)
Les tests négatifs sont la partie qui a le plus de valeur. Un token valide accepté ne prouve pas grand-chose ; c'est le token invalide accepté qui révèle un backend qui ne vérifie rien, et donc une faille de sécurité à remonter immédiatement.
Exemple concret : formulaire de contact et inscription à la newsletter
Prenons un cas réaliste pour une petite application SaaS francophone : un formulaire de contact protégé par reCAPTCHA v2 et une inscription à la newsletter protégée par Turnstile. La suite ci-dessous enchaîne, pour chaque endpoint, le test positif puis les deux tests négatifs.
tester = EndpointTester("YOUR_API_KEY")
configs = [
{
"name": "Contact form submission",
"url": "https://example.com/api/contact",
"captcha_type": "recaptcha_v2",
"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"pageurl": "https://example.com/contact",
"captcha_field": "g-recaptcha-response",
"payload": {
"name": "Test User",
"email": "test@example.com",
"message": "Automated test message",
},
"expected_status": 200,
"expected_contains": "success",
},
{
"name": "Newsletter signup",
"url": "https://example.com/api/subscribe",
"captcha_type": "turnstile",
"sitekey": "0x4AAAA...",
"pageurl": "https://example.com/newsletter",
"captcha_field": "cf-turnstile-response",
"payload": {
"email": "test@example.com",
},
"expected_status": 200,
},
]
report = tester.run_suite(configs)
print(report)
Sortie :
Endpoint Tests: 5/6 passed
==================================================
[PASS] Contact form submission (18.5s)
[PASS] Contact form submission (invalid token) (0.3s)
[PASS] Contact form submission (missing token) (0.2s)
[PASS] Newsletter signup (14.2s)
[FAIL] Newsletter signup (invalid token) (0.3s)
Error: Endpoint accepted invalid CAPTCHA token
[PASS] Newsletter signup (missing token) (0.2s)
Le résultat est parlant : l'inscription à la newsletter accepte un token bidon. Le formulaire de contact, lui, valide correctement. Vous savez exactement quel endpoint corriger, sans avoir ouvert un seul onglet de navigateur.
Questions fréquentes
Faut-il un navigateur pour tester un endpoint protégé par CAPTCHA ?
Non. Tant que la seule barrière est le CAPTCHA, vous récupérez un token via l'API et vous l'envoyez dans le payload comme n'importe quel champ de formulaire. Le navigateur n'est utile que si l'endpoint dépend d'un état JavaScript côté client.
Quels types de CAPTCHA cette approche couvre-t-elle ?
reCAPTCHA v2, reCAPTCHA v3 et Cloudflare Turnstile, qui couvrent la grande majorité des formulaires publics. À noter : hCaptcha et FunCaptcha ne sont pas pris en charge par CaptchaAI, il faudra donc les gérer autrement dans vos suites de tests.
Combien coûte la résolution pendant une campagne de tests ?
La facturation est par thread simultané, pas par résolution. Le plan BASIC ($15/mois, 5 threads) suffit largement pour une suite de tests nocturne, puisque chaque thread traite un CAPTCHA à la fois avec des résolutions illimitées sur le mois.
Comment intégrer ces tests dans une pipeline CI/CD ?
Stockez votre clé API dans un secret du pipeline (GitHub Actions, GitLab CI), lancez la suite comme un job dédié et faites échouer le build si un test négatif passe. Prévoyez quelques secondes de marge par requête, le temps de la résolution.
Dépannage des tests d'endpoints
Quatre symptômes reviennent souvent quand on soumet des tokens en dehors d'un navigateur. Voici comment les diagnostiquer :
| Problème | Cause probable | Correctif |
|---|---|---|
| Token valide rejeté | Le token a expiré avant l'envoi de la requête | Réduisez le délai entre la résolution et la soumission |
| Token invalide accepté | Le backend ne vérifie pas le token CAPTCHA | Ouvrez un ticket : c'est une faille de sécurité à corriger |
| 403 sur toutes les requêtes | Token CSRF ou cookies de session manquants | Ajoutez les cookies de session ou l'en-tête CSRF attendu |
| L'endpoint JSON refuse les données de formulaire | Type de contenu inadapté | Passez json_body: True dans la configuration |
Guides connexes
Validez chaque endpoint protégé par CAPTCHA de bout en bout — ouvrez un compte CaptchaAI.