Skip to content

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 5 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 os
import re
import time
import 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 cookie
sitekey = 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.Session keeps both together.
  • The action. The plugin sets data-action per 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

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.

Questions

What is the Simple Cloudflare Turnstile plugin?

A free WordPress plugin by Elliot Sowersby and RelyWP, now titled "Simple CAPTCHA with Cloudflare Turnstile", that adds Cloudflare Turnstile to WordPress, WooCommerce and many form plugins. It has more than 200,000 active installations.

Where is the Cloudflare Turnstile sitekey on a WordPress page?

In the widget's div, as data-sitekey. The Simple Cloudflare Turnstile plugin writes a div with the class cf-turnstile, the sitekey and a data-action naming the form, into the page's HTML.

Why does the submit button stay disabled after I insert a token?

The plugin's Disable Submit Button option keeps the button unclickable until the widget itself reports success. When you supply the token yourself, submit the form directly, for example with form.requestSubmit() or an HTTP POST.

Read next

This article is part of the Cloudflare Turnstile solver hub. Every task is charged only when a token is ready.

Get an API key