10 KiB
Cloudflare Turnstile Auto-Registration Patterns
Core Problem: Turnstile IP Binding
Cloudflare Turnstile binds the solved token to the IP that solved the challenge. The registration request MUST come from the same IP. If the captcha service solves from its IP and the registration request comes from a different IP → TURNSTILE_VERIFICATION_FAILED.
Failure Signature
{"code":400,"message":"turnstile verification failed","reason":"TURNSTILE_VERIFICATION_FAILED"}
This is NOT a bad token, wrong sitekey, or wrong endpoint. It's always IP mismatch.
Solution Matrix
| Approach | Requires | Works? | Notes |
|---|---|---|---|
TurnstileTaskProxyless + direct API |
Captcha service only | ❌ | IP mismatch (captcha IP ≠ your IP) |
TurnstileTask + datacenter proxy |
Proxy + captcha | ❌ | Cloudflare detects datacenter IPs |
TurnstileTask + residential proxy |
Residential proxy + captcha | ✅ | Both solve & register through same residential IP |
| Browser automation (Selenium/Playwright) | Local Chrome, no sandbox | ⚠️ | Turnstile iframe may not render if bot detected |
| Browser + extracted token → API | Local Chrome + API | ✅ | Same IP for solve + request |
Anti-Captcha API Patterns
Proxyless (DON'T USE — IP mismatch)
# Anti-Captcha
r = requests.post('https://api.anti-captcha.com/createTask', json={
'clientKey': KEY,
'task': {
'type': 'TurnstileTaskProxyless',
'websiteURL': 'https://target.com/register',
'websiteKey': '0x4AAAAA...',
}
})
# Token will be bound to Anti-Captcha's IP → registration fails
With Proxy (USE THIS)
r = requests.post('https://api.anti-captcha.com/createTask', json={
'clientKey': KEY,
'task': {
'type': 'TurnstileTask',
'websiteURL': 'https://target.com/register',
'websiteKey': '0x4AAAAA...',
'proxyType': 'http', # or socks5
'proxyAddress': '[REDACTED-IP]',
'proxyPort': 8080,
'proxyLogin': 'user', # optional
'proxyPassword': 'pass', # optional
}
})
# Then register through the SAME proxy
resp = requests.post('https://target.com/api/register',
json={...},
proxies={'http': 'http://user:pass@[REDACTED-IP]:8080',
'https': 'http://user:pass@[REDACTED-IP]:8080'})
Proxy Requirements
Residential proxies MANDATORY
- Datacenter IPs (Alibaba Cloud, AWS, DigitalOcean, etc.) are detected and rejected
- Cloudflare runs ASN checks — residential ISP IPs pass, cloud/hosting IPs fail
- Free proxy lists are 99% datacenter → won't work
ProxyShard Residential Format
host:port:username:password
relay-eu.proxyshard.com:8080:plan-premium-country-de-sid-XXXXX-ttl-8000000:5knun1bleejnw5m1
- Username:
plan-premium-country-{code}-sid-{session_id}-ttl-{ttl} - Password: same for all proxies in a batch
- PITFALL: Credentials can be invalid (407) if plan expired or password changed
- PITFALL: Requires IP whitelisting on proxyshard.com dashboard
Proxy Authentication Diagnostics
# Test proxy with curl
curl -v --connect-timeout 10 \
-x "host:port" \
-U "username:password" \
"https://httpbin.org/ip"
# 407 = auth failed ("Invalid credentials" in Proxy-Authenticate header)
# 200 = proxy works
Proxy URL Format for Python requests
proxy_url = f'http://{username}:{password}@{host}:{port}'
proxies = {'http': proxy_url, 'https': proxy_url}
Browser Automation with Turnstile
Selenium undetected-chromedriver
IMPORTANT: undetected-chromedriver requires Python 3.11 (or 3.12). Python 3.14 removed distutils.version.LooseVersion which the library imports. Use: C:/Users/User/AppData/Local/Python/Python311/python.exe.
import undetected_chromedriver as uc
from selenium_stealth import stealth
options = uc.ChromeOptions()
options.add_argument('--no-sandbox')
options.add_argument('--disable-blink-features=AutomationControlled')
driver = uc.Chrome(options=options, version_main=150) # match Chrome version
stealth(driver, languages=['en-US', 'en'], vendor='Google Inc.',
platform='Win32', webgl_vendor='Intel Inc.',
renderer='Intel Iris OpenGL Engine', fix_hairline=True)
# CDP evasions
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'plugins', {get: () => [1,2,3,4,5]});
window.chrome = {runtime: {}};
'''
})
Turnstile Iframe Detection
When undetected-chromedriver works: Turnstile is NOT in an iframe — it renders inline. The token is in a hidden input:
# Simple extraction (undetected-chromedriver — Turnstile is inline, not iframe)
token = [REDACTED]
"return document.querySelector('[name=\"cf-turnstile-response\"]')?.value || ''"
)
# Also check turnstile API
if not token:
token = [REDACTED]"return window.turnstile?.getResponse?.() || ''")
When headless Chrome is used (DON'T — Turnstile blocks it): Turnstile is in an iframe:
# Check if Turnstile iframe loaded
iframes = driver.find_elements(By.TAG_NAME, 'iframe')
for f in iframes:
src = f.get_attribute('src') or ''
if 'challenges.cloudflare' in src:
# Turnstile found — click the checkbox
driver.switch_to.frame(f)
checkbox = driver.find_element(By.CSS_SELECTOR, '#checkbox, [role=checkbox]')
checkbox.click()
driver.switch_to.default_content()
Forced Turnstile Render via JS
# If widget not in DOM, force-render via turnstile API
result = driver.execute_script('''
return new Promise((resolve) => {
var div = document.createElement('div');
document.body.prepend(div);
turnstile.render(div, {
sitekey: '0x4AAAAA...',
callback: (token) => resolve({status: 'ok', token: token}),
'error-callback': () => resolve({status: 'error'}),
'timeout-callback': () => resolve({status: 'timeout'})
});
setTimeout(() => resolve({status: 'timeout'}), 30000);
});
''')
# PITFALL: script timeout if Cloudflare detects bot → challenge never auto-solves
Chrome Version Matching
# Check Chrome version first
# chrome://version/ → e.g. 150.0.7871.182
# Pass version_main to match:
driver = uc.Chrome(options=options, version_main=150)
Registration API Pattern
baaaai.com / Sub2API Example
# Sitekey from page JS: window.__APP_CONFIG__.turnstile_site_key
# Or from <script>: sitekey: '0x4AAAAAAD38oqy2OVfVxy_y'
# Endpoint
REGISTER_URL = 'https://baaaai.com/api/v1/auth/register'
# Payload
payload = {
'email': 'user@domain.com',
'password': 'Password123!',
'cf-turnstile-response': token, # from captcha service or browser
'agree': True,
}
# Headers
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://baaaai.com',
'Referer': 'https://baaaai.com/register',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
}
# Whitelisted email domains (from __APP_CONFIG__)
# ["@qq.com", "@gmail.com", "*.edu.cn", "@outlook.com", "@foxmail.com", "@163.com"]
curl_cffi for TLS Fingerprint Matching
from curl_cffi import requests as cr
# Use chrome impersonation to match browser TLS fingerprint
resp = cr.post(url, json=payload, headers=headers,
impersonate='chrome131', timeout=30)
# Supported: chrome124, chrome131, etc.
# NOT supported: chrome138 (too new)
2captcha vs Anti-Captcha
2captcha
- API:
https://api.2captcha.com/createTask - Same task format as Anti-Captcha
- Error codes:
ERROR_KEY_DOES_NOT_EXIST,ERROR_ZERO_BALANCE, IP bans
Anti-Captcha
- API:
https://api.anti-captcha.com/createTask - Balance check:
POST /getBalancewithclientKey - Current key:
[REDACTED-KEY]($48.38 as of 2026-07-27)
Pitfalls
- TurnstileTaskProxyless always fails for registration — token is IP-bound to captcha service, not your IP
- Datacenter proxies always fail — Cloudflare detects ASN, rejects even with IP matching
- Free proxies are datacenter — proxy lists from proxyscrape.com, geonode.com etc. are 99% datacenter
- ProxyShard credentials can expire — 407 "Invalid credentials" means plan expired or password changed
- Browser Turnstile iframe may not render — if Cloudflare detects automation, widget shows challenge requiring human interaction,
script timeoutin Selenium - Chrome version mismatch —
undetected_chromedriverdownloads chromedriver matchingversion_main; auto-detect may download wrong version - Greenlet broken on Python 3.14 —
playwrightdepends ongreenlet._greenletwhich doesn't compile on 3.14+; use Selenium instead excludeSwitchesanduseAutomationExtensiondeprecated — newer Selenium versions reject these options- Turnstile token is a QUERY PARAMETER, not body field — On "New API" forks (getunikey.ai, etc.), the token is sent as
?turnstile=TOKEN&hcaptcha=HCAPTCHAin the URL, NOT ascf-turnstile-responsein the JSON body. Sending it in the body returns"Turnstile token 为空". The JS code pattern isFH.post(url, body, {params: {turnstile, hcaptcha}}). - Headless Chrome vs undetected-chromedriver — Headless Chrome (CDP,
--headless,mcp__chrome_devtools__*) is ALWAYS detected by Turnstile: the iframe shows "Checking your browser..." forever, buttons stay disabled.undetected_chromedriver(Python 3.11 only — distutils removed in 3.14) reliably bypasses this: Turnstile auto-solves in ~5-8s, all buttons become enabled.browser_navigate(Browserbase) also fails with Turnstile — buttons remain disabled. - Turnstile solved but token extraction fails — With undetected-chromedriver, the hidden
<input name="cf-turnstile-response">contains the token. But if the page is navigated away from or refreshed, the token is invalidated. Always extract the token BEFORE navigating. Also, the Turnstile token is single-use: once sent to the server (even if the request fails for another reason like hCaptcha), the token is consumed and a new one is needed.