Set Up Cloudflare Turnstile for Your Website
Table of Contents
- What you need before you start
- Step 1: Open Turnstile in the Cloudflare dashboard
- Step 2: Add a widget
- Step 3: Add your custom domain as a hostname
- Step 4: Choose the widget mode
- Step 5: Create the widget and copy the keys
- Step 6: Add the widget to your form
- Step 7: Validate every token on the server
- Test with Cloudflare’s dummy keys
- Troubleshooting
- Turnstile is a bot check, not access control
- What you need before you start
- Step 1: Open Turnstile in the Cloudflare dashboard
- Step 2: Add a widget
- Step 3: Add your custom domain as a hostname
- Step 4: Choose the widget mode
- Step 5: Create the widget and copy the keys
- Step 6: Add the widget to your form
- Step 7: Validate every token on the server
- Test with Cloudflare’s dummy keys
- Troubleshooting
- Turnstile is a bot check, not access control
What you need before you start
Cloudflare Turnstile tests if a form submission comes from a real visitor. Usually, the visitor does not have to solve a puzzle. To use Turnstile on your own domain, you need two keys from Cloudflare. The sitekey is public and goes in the HTML of your page. The secret key is private and stays on the server that receives the form.
You also need a Cloudflare account. Your site does not have to use the DNS or the proxy of Cloudflare. Turnstile works on any website, on any host.
You need your own keys when your site on your own domain accepts submissions from visitors. Examples are contact, feedback or signup forms, comments and all other features that write data. A read-only site does not need Turnstile.
Each pair of keys belongs to a widget, and each widget has a list of hostnames. A widget works only on the domains that you add to it. For this reason, each custom domain needs a widget that lists it.
Step 1: Open Turnstile in the Cloudflare dashboard
- Sign in to the Cloudflare dashboard.
- Open Turnstile from the account sidebar.
Step 2: Add a widget
- Select Add widget.
- Enter a Widget name that you will recognize later, for example
Contact form - example.com.
One widget can serve several pages and forms on the same domains. If you want different settings, create separate widgets, for example one for each site.
Step 3: Add your custom domain as a hostname
Under Hostname management, add the domain of the form (hostname management). Follow these rules:
- Enter only the bare domain:
example.com, nothttps://example.com,example.com/contactorexample.com:8080. Cloudflare rejects protocols, paths and ports. - A root domain covers its subdomains. When you add
example.com, it also coverswww.example.comandapp.example.com. - The form can also show on the default address of your host, such as
<project>.pages.devor<site>.netlify.app. In that case, add that hostname too. - Free accounts can add up to 10 hostnames for each widget.
For local development, do not add localhost to your production widget. Use the Cloudflare test keys instead (see below).
Step 4: Choose the widget mode
Cloudflare offers three modes (widget types):
| Mode | What the visitor sees |
|---|---|
| Managed (recommended) | Usually nothing to do. A checkbox appears only when Cloudflare wants more verification |
| Non-interactive | A visible widget with a loading spinner. It never asks the visitor to act |
| Invisible | Nothing at all. Your privacy policy must refer to Cloudflare’s Turnstile Privacy Addendum |
If you have no reason to use a different mode, select Managed. Keep Pre-clearance off. Turn it on only if you run a single-page app and know that you need it.
Step 5: Create the widget and copy the keys
Select Create. Cloudflare shows the two keys (dashboard widget management):
| Key | What it is | Where it goes |
|---|---|---|
| Sitekey | Public identifier for the widget | In the HTML of the page with the form |
| Secret key | Private key to validate tokens | In the environment variables or secrets of your server. Never put it in HTML, browser JavaScript or a public repository |
Step 6: Add the widget to your form
Load the Cloudflare script. Then put the widget inside the form (client-side rendering):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form action="/api/contact" method="POST">
<label>Email <input type="email" name="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<div class="cf-turnstile" data-sitekey="YOUR-SITEKEY"></div>
<button type="submit">Send</button>
</form>
When the visitor passes the challenge, Turnstile adds a hidden field named cf-turnstile-response to the form. The field holds a token (a short-lived proof) that your server must validate.
Load api.js from that exact URL. Do not proxy or cache the file yourself. Cloudflare warns that a proxied or cached copy breaks Turnstile when Cloudflare updates the file.
Step 7: Validate every token on the server
The widget alone is not enough. Anyone can send a request directly to your form endpoint and skip the page. Server-side validation is required. Your server sends the token and your secret key to the Siteverify endpoint of Cloudflare. The server accepts the form only when the answer is success: true.
Know these rules:
- The Siteverify address is
https://challenges.cloudflare.com/turnstile/v0/siteverify. It accepts aPOSTwithsecretandresponse(the token), and an optionalremoteip. - A token expires 300 seconds after Turnstile creates it.
- You can validate a token only one time. A second validation fails with
timeout-or-duplicate. - The response includes the
hostnamewhere Turnstile issued the token. You can compare it with your domain.
This complete example is for a site on Cloudflare Pages. It is a Pages Function in functions/api/contact.js. It handles POST /api/contact from the form above:
export async function onRequestPost({ request, env }) {
const form = await request.formData();
const token = form.get("cf-turnstile-response") || "";
const check = new FormData();
check.append("secret", env.TURNSTILE_SECRET_KEY);
check.append("response", token);
const ip = request.headers.get("CF-Connecting-IP");
if (ip) check.append("remoteip", ip);
const result = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ method: "POST", body: check }
).then((r) => r.json());
if (!result.success) {
return new Response("Verification failed. Please reload and try again.", { status: 403 });
}
// The visitor passed: send the email, save the message, and so on.
return new Response("Thanks, we received your message.");
}
To store the secret, open Workers & Pages. Select the project. Go to Settings > Variables and Secrets > Add. Name the variable TURNSTILE_SECRET_KEY and paste the secret. Select Encrypt. Then save (Pages bindings).
Drag-and-drop uploads in the dashboard do not compile a functions folder. For this reason, deploy with Wrangler or from Git (Direct Upload).
On other hosts, the logic is the same in any server language. Read cf-turnstile-response. Post it to Siteverify with your secret. If success is not true, reject the request. Netlify Functions, Vercel Functions and your own backend all work.
Test with Cloudflare’s dummy keys
Cloudflare publishes test keys that work on any domain, including localhost:
| Test sitekey | Behavior |
|---|---|
1x00000000000000000000AA |
Always passes (visible widget) |
2x00000000000000000000AB |
Always fails (visible widget) |
3x00000000000000000000FF |
Forces an interactive challenge |
| Test secret key | Behavior |
|---|---|
1x0000000000000000000000000000000AA |
Always passes validation |
2x0000000000000000000000000000000AA |
Always fails validation |
3x0000000000000000000000000000000AA |
Returns timeout-or-duplicate |
Test sitekeys produce the dummy token XXXX.DUMMY.TOKEN.XXXX. Only test secret keys accept this token. Use test keys in development and real keys in production. Set the keys through environment variables. Do not ship the test keys.
Troubleshooting
- If the widget does not appear on your domain, or shows an error, the domain is probably missing from the hostname list (Step 3). Compare the list with the exact domain in the address bar of the browser. Include any subdomain that the root entry does not cover.
- Sometimes the widget works on the default address of the host but not on the custom domain, or the reverse. In that case, add both hostnames to the widget.
- If Siteverify returns
invalid-input-secret, the secret on the server is wrong or empty. Make sure that the variable name and the value are correct. - If Siteverify returns
missing-input-responseorinvalid-input-response, the token is missing or malformed. Make sure that the widget is inside the<form>element. Also make sure that your server reads thecf-turnstile-responsefield without changes. - If Siteverify returns
timeout-or-duplicate, the token was older than five minutes or a server already validated it. After a failed submission, reset the widget, so that the visitor gets a new token. - If the secret key leaked, open the widget’s Settings. Then use Rotate Secret Key. The old and the new secret both work for about two hours while you update your server (rotate the secret key).
Turnstile is a bot check, not access control
Turnstile tells you that a submission probably came from a person. It does not identify an approved client, and it does not keep a private page private. If a site is only for certain people, also use the access control of your host. Our guides cover Cloudflare Pages with Cloudflare Access and Netlify password protection.
Free email addresses
for your AI agents
Each agent gets an address like myagent.x7k2@revdokumail.com. It can receive emails, send replies and store files.
- 3 free mailboxes, each with its own address
- 3,000 incoming and 500 sent emails a month
- 3 GB of storage for attachments and files
- Virus, malware, spam and prompt-injection filter
- Read and reply through API, MCP or CLI
You do not need a card.
Frequently Asked Questions
Does Turnstile replace rate limiting?
No. Turnstile filters automated submissions, but a real person can still submit a form many times. Keep server-side validation. If abuse is a concern, also add a rate limit on the form endpoint. A rate limit sets a maximum number of requests in a time period.
Related Articles

Add a Custom Domain to a Cloudflare Pages Site
Add a custom domain or www to a Cloudflare Pages site: set it up in Pages first, add the DNS record, fix stuck verification and redirect www.

What Is localhost:3000?
Your browser uses localhost:3000 to reach an app running on your own computer. Sending that address to someone else does not give them access to your app.

Move Exported Grok Website Files to Another Host
Export a Grok Build site to GitHub, deploy it on Cloudflare Pages, Netlify, Vercel or GitHub Pages, test it, then retire the grok.me copy.