Zwei Läufe gegen dieselbe Domain, zwei Ergebnisse: Der Login wird sauber gelöst, der Checkout läuft ins Timeout. Der Grund steht meist eine Zeile über dem Fehler – method ist fest auf userrecaptcha gesetzt, ausgeliefert wird an dieser Stelle aber Cloudflare Turnstile. Die Korrektur ist unspektakulär, aber strukturell: Bestimmen Sie den Anbieter bei jedem Seitenaufruf neu und leiten Sie Methode, Sitekey-Parameter und Antwortfeld daraus ab. Dieser Leitfaden zeigt die Marker beider Anbieter im Quelltext, eine fertige Erkennungsroutine in Python und JavaScript sowie die typischen Fehlerbilder.
reCAPTCHA v2 oder Turnstile: Anbieter im Quelltext erkennen
Beide Widgets tragen ihren Sitekey im Attribut data-sitekey – dieses Attribut allein sagt also nichts über den Anbieter aus. Eindeutig wird es erst über die CSS-Klasse und die nachgeladene Skript-URL.
| Anbieter | HTML-Marker | Skript-URL | Antwortfeld |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
Zwei Hinweise zur Tabelle: Prüfen Sie immer zuerst auf cf-turnstile und erst danach auf g-recaptcha, denn ein einzelnes data-sitekey ohne Klassenkontext führt regelmäßig zur falschen Zuordnung. Die hCaptcha-Zeile wiederum steht hier ausschließlich zur Abgrenzung: CaptchaAI unterstützt hCaptcha nicht, aber wer den Marker sauber erkennt, merkt früh, dass dieser Pfad im Workflow gesondert behandelt oder kontrolliert abgebrochen werden muss.
Warum eine Website zwei Anbieter parallel ausliefert
| Szenario | Wie es sich im Traffic zeigt |
|---|---|
| Getrennte Seitenbereiche | Login mit reCAPTCHA v2, Checkout mit Turnstile |
| A/B-Test zweier Anbieter | Dieselbe URL zeigt je Session zufällig den einen oder den anderen Typ |
| Laufende Migration | Alte Templates tragen reCAPTCHA, neu ausgerollte Seiten Turnstile |
| Fallback-Kette | Der primäre Anbieter lädt nicht, das Frontend blendet den zweiten ein |
| Regionale Ausspielung | Besucher aus den USA sehen reCAPTCHA, Besucher aus der EU Turnstile |
Ein Fall aus dem DACH-Alltag
Ein Shopware-Shop, gehostet bei Hetzner, stellt den Checkout auf Cloudflare Turnstile um, nachdem der Datenschutzbeauftragte den Einsatz von Google-Diensten im Bestellprozess hinterfragt hat. Der Login-Bereich bleibt vorerst bei reCAPTCHA v2, weil dort ein älteres Theme im Einsatz ist. Die End-to-End-Tests des eigenen QA-Teams kennen nur userrecaptcha – und brechen ab dem ersten Deployment im Checkout ab, obwohl an der Testlogik nichts geändert wurde.
Dieser Zwischenzustand ist der Normalfall: Staging und Produktion machen solche Umstellungen selten am selben Tag mit; in der GitLab-CI-Pipeline fällt der Bruch erst im roten Job auf. Eine dynamische Erkennung überbrückt die Migrationsphase, ohne doppelten Testcode. Nebenbei gilt: Sobald Proxys oder Logs mit IP-Adressen im Spiel sind, gehören Datenflüsse und Rechtsgrundlage nach DSGVO in dieselbe Prüfung – IP-Adressen zählen als personenbezogene Daten.
reCAPTCHA v2 und Turnstile in Python erkennen und lösen
Die folgende Routine ruft die Seite ab, prüft zuerst auf Turnstile, dann auf reCAPTCHA v2 – inklusive der per grecaptcha.render() nachgeladenen Variante – und liefert ein CaptchaInfo-Objekt zurück, das Methode, Sitekey und Antwortfeld zusammenhält. solve_captcha() übermittelt die Aufgabe an in.php und fragt das Ergebnis anschließend im Fünf-Sekunden-Takt über res.php ab.
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://example.com/login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
Der Kern ist die Zuordnung am Ende: Aus dem erkannten Anbieter ergeben sich drei Werte, die zusammenpassen müssen.
| Anbieter | method |
Sitekey-Parameter | Antwortfeld | Lösungszeit (Obergrenze) |
|---|---|---|---|---|
| reCAPTCHA v2 | userrecaptcha |
googlekey |
g-recaptcha-response |
< 60 s |
| Cloudflare Turnstile | turnstile |
sitekey |
cf-turnstile-response |
< 10 s |
Die Zeitangaben stammen von den öffentlichen Solver-Seiten; sie markieren die obere Grenze, nicht den Durchschnitt. Für die Praxis heißt das vor allem: Ein Turnstile-Lauf gibt seinen Thread deutlich früher wieder frei als ein reCAPTCHA-v2-Lauf.
Dieselbe Logik in JavaScript
In Node.js bleibt der Ablauf identisch, nur kompakter: detectCaptchaType() liefert ein Objekt mit method, sitekey und field, solveCaptcha() übernimmt Übermittlung und Polling.
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://example.com/login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
Bei serverseitig geliefertem HTML genügt der einfache fetch()-Aufruf. Wird das Widget erst im Browser eingehängt, holen Sie den Quelltext aus einer Playwright- oder Puppeteer-Sitzung – die Erkennungsfunktion selbst bleibt unverändert.
Fehlerbehebung
| Symptom | Ursache | Vorgehen |
|---|---|---|
| Falscher Typ erkannt | Die Regex greift auf data-sitekey zu, bevor sie die Anbieterklasse geprüft hat |
Erst cf-turnstile bzw. g-recaptcha matchen, dann den Sitekey auslesen |
| Token wird abgelehnt, obwohl die Lösung ankam | Das Token steht im Feld des falschen Anbieters | Antwortfeld strikt aus der Erkennung ableiten: cf-turnstile-response oder g-recaptcha-response |
| Der Typ wechselt zwischen zwei Aufrufen | A/B-Test oder regionale Ausspielung | Pro Request neu erkennen und den Anbieter nie aus dem ersten Besuch zwischenspeichern |
| Beide Widgets im Quelltext | Eines ist ausgeblendet oder inaktiv | Sichtbarkeit und gefüllten data-sitekey prüfen, nur das aktive Widget lösen |
| Erkennung findet nichts | Das Widget wird per Skript nachgeladen | Auf grecaptcha.render() bzw. turnstile.render() prüfen oder die gerenderte Seite auswerten |
| Test läuft, Produktion scheitert | Session, Header oder Proxy-Kontext weichen vom Test ab | Erfolgreiche Testbedingungen unverändert übernehmen und Solver-Typ, Latenz, Fehlercode gemeinsam protokollieren |
Was die Doppelabdeckung kostet
Zwei Anbieter bedeuten weder zwei Verträge noch zwei API-Schlüssel. CaptchaAI rechnet Thread-basiert ab: Sie zahlen für gleichzeitig laufende Lösungen, nicht pro gelöstem CAPTCHA, und die Zahl der Lösungen pro Thread ist im Monatstarif nicht gedeckelt. Ob ein Thread gerade userrecaptcha oder turnstile bearbeitet, ändert am Preis nichts.
Relevant wird der Anbietermix erst bei der Kapazitätsplanung, weil ein reCAPTCHA-v2-Lauf einen Thread länger belegt als ein Turnstile-Lauf. Für einzelne Testpipelines reicht BASIC (15 $/Monat, 5 Threads); wer parallele Scraping-Worker über mehrere Seitentypen fährt, landet eher bei ADVANCE (90 $/Monat, 50 Threads). Die Preise sind in US-Dollar ausgewiesen.
Häufige Fragen
Wie lange bleibt ein gelöstes Token gültig?
Kurz – planen Sie mit rund zwei Minuten. Ein reCAPTCHA-v2-Token verfällt nach etwa 120 Sekunden, Turnstile-Token sind Einmal-Token und werden serverseitig eingelöst. Lösen Sie deshalb erst unmittelbar vor dem Absenden des Formulars und legen Sie keine Token auf Vorrat an.
Brauche ich für beide Anbieter getrennte Zugangsdaten?
Nein. Ein API-Schlüssel, dieselben Endpunkte in.php und res.php, nur ein anderer Wert in method und ein anderer Parametername für den Sitekey. Genau deshalb lässt sich die Fallunterscheidung in eine einzige Funktion kapseln.
Was passiert mit der Laufzeit, wenn statt Turnstile plötzlich reCAPTCHA v2 kommt?
Sie steigt spürbar. Turnstile wird typischerweise in unter 10 Sekunden gelöst, reCAPTCHA v2 in unter 60 Sekunden. Wer knapp gesetzte Timeouts in der Pipeline hat, sollte sie am langsameren der beiden Typen ausrichten, nicht am schnelleren.
Muss die Erkennung im Headless-Browser laufen?
Nur wenn das Widget nicht im ausgelieferten HTML steht. Ein reiner HTTP-Request ist schneller; sobald das Formular erst clientseitig entsteht, brauchen Sie das gerenderte DOM aus Selenium, Playwright oder Puppeteer.
Was tun, wenn eine Seite auf hCaptcha umstellt?
Die Erkennung fängt den Fall sauber ab, gelöst wird er nicht: hCaptcha gehört nicht zu den von CaptchaAI unterstützten Typen. Behandeln Sie den Marker als eigenen Zweig, protokollieren Sie ihn und brechen Sie den Lauf kontrolliert ab, statt ihn in ein Timeout laufen zu lassen.