I stumbled upon Cloudflare Turnstile when I had to log into my GenerateBlocks account. On previous login attempts, GenerateBlocks would send a code via email that you would then have to enter. Super inconvenient, especially if you can’t access the purchaser’s email account. So I was pleasantly surprised when I saw GenerateBlocks change up their bot protection.

So I did some digging. It turns out the login form is using Cloudflare Turnstile to validate its users.
What is Turnstile?
Cloudflare Turnstile is an alternative to a CAPTCHA, and seems to be competing directly with reCAPTCHA 3. It can be embedded in forms, logins, or any place you need to validate that someone is, in fact, human.
Cloudflare’s CDN already offers its customers a Challenge page if the visitor seems spammy or malicious. This same challenge now comes in widget form without having to use Cloudflare’s CDN. This means that Turnstile is a free solution.
The widget outlays a challenge, often without interaction, for the end user. This challenge is adaptable. Meaning if one challenge is not working well, then the challenge can be updated without any code modifications on your end. So it’s a one and done implementation, in theory.
Opting in to Turnstile
It turns out that if you have a Cloudflare account, you have a new Turnstile tab. Note the beta tag.
From there, you can set up a site that will utilize the Turnstile service.
Once you click on “Add Site,” you’re taken to a screen where you can add a site name, add a domain, and select your widget type.
For my case, I chose the “Non-interactive” version.
Once you choose to “Create” the site, you’ll be shown instructions and receive a Site Key and a Secret Key.
Adding Turnstile to your site
Now here’s the rub. Turnstile is relatively new, so not everything has a solution yet.
For WordPress sites, however, there is a very versatile plugin called Simple Cloudflare Turnstile, which integrates natively with many solutions.
I like the idea of a plugin that integrates with many solutions, but I really only cared about one type of implementation. Comments.
Turnstile and WordPress comments
I have a premium plugin called Comment Edit Pro, which integrates with several anti-spam services such as reCAPTCHA 3 and Akismet.
Since I was so impressed with Turnstile and how it worked, I wanted to add a native integration specific to comments. Yes, the plugin above does comments, but since I already integrated reCAPTCHA 3 into Comment Edit Pro, I felt Turnstile needed to be added too.
So that takes me back to square one. I needed to develop my own solution.
Getting started?
On the Turnstile Getting Started page, you’re shown a script you can include that can run in the <head>
element.
About script placement…
I’ve found that it doesn’t matter where you place the script, but that’s just from my testing.
You’re then shown steps for the client-side (user-facing) integration. You can choose between Explicit and Implicit rendering.
Implicit rendering
An implicit render is more of a hands-off approach, hence the name implicit. You just need to add a sliver of HTML to your form.
But first you’ll need to add the Turnstile script to your page.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Code language: HTML, XML (xml)
After that, find your form’s source and add in the HTML.
<form method="POST" action="/handler">
<!-- form inputs and fields -->
<div class="cf-turnstile" data-sitekey="1x00000000000000000000AA"></div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
</form>
Code language: HTML, XML (xml)
Just add a div
with class cf-turnstile
and sitekey
data attribute.
Make sure you place the div inside of your form tags.
Turnstile will then inject a hidden input
element named cf-turnstile-respons
e, which will contain a token if a challenge is passed.
<input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-mgegk_response" value="XXXX.DUMMY.TOKEN.XXXX">
Code language: HTML, XML (xml)
Once the form is submitted, you’d check for the cf-turnstile-response
post variable on the server-side, validate it against a Turnstile endpoint, and either complete or reject the form.
Explicit rendering
Explicit rendering, as in, you are actively doing something, is a more hands-on approach. You’ll be manually rendering Turnstile as opposed to the widget being shown automatically.
The script tag is the same except for a callback function when the Turnstile script is done initializing.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" async defer></script>
Code language: HTML, XML (xml)
In this example, there is an onload
callback named onloadTurnstileCallback
.
You can name this callback function anything you like, but if you have onloadTurnstileCallback
as the callback, you must create this callback in your own local JavaScript files.
/**
* This is the main callback for Cloudflare.
*/
window.onloadTurnstileCallback = () => {
// do turnstile explicit rendering stuff here.
};
Code language: JavaScript (javascript)
The example Turnstile provides for explicitly rendering the widget is:
window.onloadTurnstileCallback = function () {
turnstile.render('#example-container', {
sitekey: '<YOUR_SITE_KEY>',
callback: function(token) {
console.log(`Challenge Success ${token}`);
},
});
};
Code language: JavaScript (javascript)
Not shown is any server-side validation (we’ll get to that).
Since this is “explicit”, we’ll have to render our own widget, get the token, and then validate it.
You can also pass in additional parameters to turnstile.render
to customize the appearance and output.
Implicit vs. Explicit
If you want hands-off on the frontend, choose Implicit.
If you want to manually add the widget, choose Explicit.
Validating the response on the server
Once your token is read in via a form variable, you’ll need to capture that on the back-end (server-side validation).
Here’s a quick example of doing that in WordPress land:

To validate on the server, you need the Turnstile secret key and the client-side token.
Since Turnstile adds the cf-turnstile-response
hidden variable in the form, you can just check the $_POST
super global for its existence.
The endpoint to verify the token is:
https://challenges.cloudflare.com/turnstile/v0/siteverify
Code language: JavaScript (javascript)
The body
of the request should contain keys: secret
and response
. Secret being your Turnstile secret key, and response being the token.
If the response is successful, it’ll pass a success
parameter in its body
response.
That’s pretty much it for server validation. For now.
Comment Edit Pro and Turnstile
For Comment Edit Pro, the first thing I needed was a UI to capture the site key and secret key.
I also added in some options to configure the Turnstile widget appearance.
The first dilemma with these options was how to get this working locally. If you remember, when enabling Turnstile for a site, you have to give an actual working domain. So how do I test if it’s working and not have to edit files directly on the server?
There are test keys. Buried in the Turnstile FAQ are the local keys you need to test with. I’ve listed them here as well.
Sitekey | Description |
---|---|
1x00000000000000000000AA | Always passes |
2x00000000000000000000AB | Always blocks |
3x00000000000000000000FF | Forces an interactive challenge |
Secret key | Description |
---|---|
1x0000000000000000000000000000000AA | Always passes |
2x0000000000000000000000000000000AA | Always fails |
3x0000000000000000000000000000000AA | Yields a “token already spent” error |
Armed with these test keys, I was ready to begin the process of explicitly setting the widget and doing the server-side validation.
Initializing the actions and filters
I had to use the wp
action as it loads pretty late and I needed a reliable mechanism for determining if comments were open or not.
Inside the action_wp
callback, I do some sanity checks to make sure that comments are open, and to ignore Turnstile for those who can moderate comments. I then load in my options, and check to see if Turnstile should be enabled for regular logged-in users.
Modifying the Submit button
For Turnstile, I needed to get the comment submit button’s HTML ID attribute value. This is so I can target the button and work backwards to get the form it’s contained in. I hooked into the comment_form_submit_button
filter to do this.
Within the callback, I check to see if Turnstile is active and replace the comment submit button’s ID with sce-turnstile-submit
.
Loading the frontend scripts
The next actions and filters involve loading the Cloudflare frontend script.
Let’s start with callback enqueue_turnstile_scripts
.
I first enqueue my local JavaScript file, which will initialize Turnstile. I then load in the Turnstile script, making sure my local script runs first.
Note the callback in the Turnstile script
When Turnstile is initialized and loaded, it will call my function sceInitTurnstile.
Lastly, I use wp_localize_script
to output the widget options that were saved in the admin. This is so I can set them on the frontend.
Up next is deferring the Cloudflare script.
A simple search/replace gets the job done here.
Adding the widget container to the comment section
Using the comment_form_defaults
filter, I can modify the output of the comment section and inject my empty widget container.
The frontend script
In my local JavaScript code, I initialize a global function that can be called via a callback.
I attempt to get the Submit button (we set the ID earlier), and I then trace backwards to get the form, so that I can then get the textarea field.
I need the textarea field because the tokens have a shelf-life of 300 seconds. I want to only initialize Turnstile if someone is actively writing a comment.
When a user starts typing into the textarea field, the widget will begin to be rendered. I did this because of the token timeout.
Tokens last for 300 seconds
If a token has expired, you’ll have to reset the widget to get another challenge token.
Comments are typically at the end of the post, so starting the widget on page load didn’t make sense in this case. The timeout of 300 seconds is so that I can refresh the widget and generate a new token.
The timeout is shown in this highlighted code section.
Calling turnstile.reset
will generate a new token and will call the callback
again with this token.
The Frontend output
If all is well, a user just has to start typing in the textarea field for Turnstile to be initialized.
Armed with a token courtesy of the injected hidden input from Turnstile, I can do server validation when the comment is submitted.
Server-side validation
Back to PHP land, I hook into the action pre_comment_on_post
to make sure that we reject a comment early.
And here’s my server-validation method in all its glory.

Conclusion
In this developer diary, I went over what Cloudflare Turnstile is, why or not you should use it, and how to implement it.
I also covered how I added the feature to comments. In fact, it’s enabled below if you want to give it a whirl. As mentioned, the widget doesn’t initialize until you start typing a comment.
Thank you so much for reading.
Nice article, Ron.
Does this service function as a replacement for Google’s recaptcha? I like the idea of one less Google service hoovering up data about me.
Sure does! It’s a lot easier to implement too.