# 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.

- Source: https://zerocaptcha.io/blog/cloudflare-turnstile-wordpress
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

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](https://zerocaptcha.io/guides/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:

```html
<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`.

```python
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](https://zerocaptcha.io/guides/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](https://zerocaptcha.io/guides/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:

```js
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](https://zerocaptcha.io/guides/submit-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](https://zerocaptcha.io/guides/test-cloudflare-turnstile-in-ci).

Whatever the plugin, the sitekey is on the page: [find a Cloudflare Turnstile sitekey](https://zerocaptcha.io/guides/find-cloudflare-turnstile-sitekey)
shows every place it can hide. The [Python page of the Cloudflare Turnstile
solver](https://zerocaptcha.io/cloudflare-turnstile-solver/python) has the same task flow as a tested program.

## Sources

- [Simple CAPTCHA with Cloudflare Turnstile on WordPress.org](https://wordpress.org/plugins/simple-cloudflare-turnstile/)
  and its [source on GitHub](https://github.com/ElliotSowersby/simple-cloudflare-turnstile),
  version 1.44.0 (checked 1 October 2026).
- [Cloudflare Turnstile community resources](https://developers.cloudflare.com/turnstile/community-resources/)
  and [client-side rendering](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/)
  (checked 1 October 2026).
- [Contact Form 7: Cloudflare Turnstile integration](https://contactform7.com/turnstile-integration/),
  [WPForms: setting up Cloudflare Turnstile](https://wpforms.com/docs/setting-up-cloudflare-turnstile/)
  and [Gravity Forms: Cloudflare Turnstile add-on](https://docs.gravityforms.com/category/add-ons-gravity-forms/cloudflare-turnstile/)
  (checked 1 October 2026).

## 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.
