Tutorial
Simple Cloudflare Turnstile on WordPress: Solving and Testing
How WordPress sites add Cloudflare Turnstile with the Simple Cloudflare Turnstile plugin, Contact Form 7, WPForms or Gravity Forms, and how to automate them.
By ZeroCaptcha Engineering5 min readPublished Updated
Most WordPress sites add Cloudflare Turnstile with a plugin, and the most common is Simple
Cloudflare Turnstile, now titled “Simple CAPTCHA with Cloudflare Turnstile” (by Elliot Sowersby
and RelyWP, more than 200,000 active installations, free). It puts a div with the class
cf-turnstile, the sitekey and a data-action naming the form into the page’s HTML, and checks
the cf-turnstile-response field on the server. To automate such a form on a site you are allowed
to, read the sitekey and action from that div, get a token from a solving API, and post it with
the form. This tutorial shows how, for the WordPress login form, and what differs for Contact
Form 7, WPForms, Gravity Forms and WooCommerce.
Automate only sites you run or have permission to automate. See responsible captcha automation.
Which forms the plugin protects
From the plugin’s page, checked 1 October 2026: WordPress login, registration, password reset and comments; WooCommerce checkout, pay for order, account details, login, registration and password reset; and forms built with WPForms, Fluent Forms, Contact Form 7, Gravity Forms, Formidable, Forminator, Jetpack, Kadence, SureForms and Elementor Pro, among others. The plugin says it is not affiliated with Cloudflare, and Cloudflare publishes no WordPress plugin for Turnstile of its own; its community resources page lists this one.
Several form plugins also have Cloudflare Turnstile built in:
- Contact Form 7 since version 6.1: set it up under Contact › Integration, and every form gets
the widget; a
[turnstile]form-tag moves it. - WPForms: WPForms › Settings › CAPTCHA › Turnstile.
- Gravity Forms: through its official Cloudflare Turnstile add-on.
What the plugin puts in the page
The plugin’s source (version 1.44.0) renders the widget as:
<div id="cf-turnstile-1" class="cf-turnstile" data-sitekey="0x4AAAAAAAB1cD2eF3gH4iJ5" data-action="wordpress-login" data-retry="auto" data-refresh-expired="auto"></div>The attributes above are shortened, and the ID and action vary by form. It loads Cloudflare’s script
with explicit rendering (api.js?render=explicit&onload=cfturnstileOnload), so the widget appears
once that script runs; the div and its sitekey are in the HTML before that, which means an HTTP
client can read them without running JavaScript. When the widget passes, Turnstile adds the hidden
cf-turnstile-response input to the form, and the plugin’s server code reads that field and calls
Cloudflare’s siteverify.
Automate the WordPress login form
This Python script logs in to a WordPress site you run, through its Cloudflare Turnstile widget. It
needs Python 3.10 or later and pip install requests; set ZEROCAPTCHA_API, ZEROCAPTCHA_KEY,
WP_USER and WP_PASSWORD.
import osimport reimport timeimport uuid
import requests
API = os.environ["ZEROCAPTCHA_API"]KEY = os.environ["ZEROCAPTCHA_KEY"]LOGIN = "https://blog.example.com/wp-login.php"
def solve_turnstile(page_url, sitekey, action=None, cdata=None): task = {"type": "TurnstileTaskProxyless", "websiteURL": page_url, "websiteKey": sitekey, "metadata": {}} # The widget's data-action and data-cdata go in metadata, only when it sets them: many sites # check both when they verify the token. if action: task["metadata"]["action"] = action if cdata: task["metadata"]["cdata"] = cdata # One Idempotency-Key per task: a retried create with it returns the same task. created = requests.post(f"{API}/createTask", json={"clientKey": KEY, "task": task}, headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=15).json() if created["errorId"]: raise RuntimeError(f"createTask: {created['errorCode']}") deadline = time.monotonic() + 180 while time.monotonic() < deadline: time.sleep(2) result = requests.post( f"{API}/getTaskResult", json={"clientKey": KEY, "taskId": created["taskId"]}, timeout=15 ).json() if result["errorId"]: raise RuntimeError(f"getTaskResult: {result['errorCode']}") if result["status"] == "ready": return result["solution"]["token"] raise TimeoutError("no token within 180 seconds")
def widget_attribute(html, name): tag = re.search(r'<div[^>]*class="cf-turnstile[^"]*"[^>]*>', html) if tag is None: return None value = re.search(rf'{name}="([^"]*)"', tag.group(0)) return value.group(1) if value else None
site = requests.Session()page = site.get(LOGIN, timeout=15) # also sets WordPress's test cookiesitekey = widget_attribute(page.text, "data-sitekey")if sitekey is None: raise SystemExit("No Cloudflare Turnstile widget on the login page")token = solve_turnstile( LOGIN, sitekey, widget_attribute(page.text, "data-action"), widget_attribute(page.text, "data-cdata"),)
reply = site.post( LOGIN, data={ "log": os.environ["WP_USER"], "pwd": os.environ["WP_PASSWORD"], "wp-submit": "Log In", "redirect_to": "https://blog.example.com/wp-admin/", "testcookie": "1", "cf-turnstile-response": token, }, timeout=15,)logged_in = any(name.startswith("wordpress_logged_in_") for name in site.cookies.keys())print("Logged in" if logged_in else f"Refused, HTTP {reply.status_code}")Three details make it work:
- One session. WordPress sets a test cookie on the login page and refuses a login without it,
and the plugin checks the token with the same request.
requests.Sessionkeeps both together. - The action. The plugin sets
data-actionper form; the task sends it back, so the token is issued for the action the form expects. See Cloudflare Turnstile action and cData. - Freshness. The token is valid for 300 seconds and for one check. If the login fails for any other reason, such as a wrong password, get a new token before trying again. See Cloudflare Turnstile token expiry.
In a browser: the disabled submit button
The plugin has a “Disable Submit Button” option: the form’s button ignores clicks
(pointer-events: none) until the widget reports success. If your browser automation writes a token
from the API into the cf-turnstile-response input itself, the widget never reports success and the
button stays disabled. Submit the form directly instead:
await page.evaluate((token) => { const form = document.querySelector('[name="cf-turnstile-response"]').closest("form"); for (const input of form.querySelectorAll('[name="cf-turnstile-response"]')) input.value = token; form.requestSubmit();}, token);requestSubmit() sends the form as a click on a submit button would, including the site’s own
submit handlers. Submit a Cloudflare Turnstile token
covers forms that read the token from a callback instead.
Contact Form 7, WPForms, Gravity Forms and WooCommerce
- Contact Form 7, WPForms and Gravity Forms often submit their forms with JavaScript (AJAX), depending on the plugin and its settings. The token still travels in the request, but the request may go to the plugin’s endpoint rather than the page. Open the browser’s Network tab, submit the form once by hand, and copy the request’s URL and fields; then send the same request with a fresh token.
- WooCommerce checkout is also an AJAX request with many fields. Automate it only on a store you run, for testing, and prefer Cloudflare’s testing sitekeys there: they need no solving at all. See test Cloudflare Turnstile in CI.
Whatever the plugin, the sitekey is on the page: find a Cloudflare Turnstile sitekey shows every place it can hide. The Python page of the Cloudflare Turnstile solver has the same task flow as a tested program.
Sources
- Simple CAPTCHA with Cloudflare Turnstile on WordPress.org and its source on GitHub, version 1.44.0 (checked 1 October 2026).
- Cloudflare Turnstile community resources and client-side rendering (checked 1 October 2026).
- Contact Form 7: Cloudflare Turnstile integration, WPForms: setting up Cloudflare Turnstile and Gravity Forms: Cloudflare Turnstile add-on (checked 1 October 2026).
The team that builds and runs the ZeroCaptcha API. Articles are drafted with AI tools, then checked against the API's code and the primary sources each one cites.