The error means one thing: when the form was submitted, the hidden field Turnstile fills with the token was still empty. Here are the causes, most common first.
After a visitor passes Cloudflare Turnstile, the widget writes a token into a hidden field named cf-turnstile-response. If the server receives the request with that field empty, it reports a missing token. So the problem is almost always that the token was not ready at the moment of submit.
Turnstile is asynchronous: it checks browser behaviour first, then generates the token and calls the callback you configured. If the submit button posts the form the instant it is clicked, that can happen before the callback. Submit from inside the callback, or set a flag there and check it before submitting.
// Implicit rendering: submit from the callback Turnstile calls for you
function onTurnstileSuccess(token) {
document.querySelector('#my-form').requestSubmit()
}
// Or explicit rendering: only let the submit through once a token exists
const widgetId = turnstile.render('#turnstile-box', {
sitekey: SITEKEY,
callback: () => { ready = true },
})
form.addEventListener('submit', (e) => {
if (!turnstile.getResponse(widgetId)) e.preventDefault()
})
A token is single use: once submitted it is spent, so do not retry with the same one.
If the Turnstile box never appears, the token stays empty. Usually the script did not load, the container did not exist, or implicit and explicit rendering were mixed. With explicit rendering, the element carrying class="cf-turnstile" needs data-render="explicit", otherwise the widget is not created.
A sitekey is bound to domains. If the page is deployed on a domain that was not added to the Turnstile configuration (for example you moved from example.com to www.example.com, or you are on a preview domain), the widget keeps failing and there is no token. In the Cloudflare dashboard, confirm the domain list for this sitekey includes the domain you are actually on.
For local development, either add localhost to the allowed domains, or use the official Cloudflare test sitekey, which always passes and is meant for integration work.
The checks above assume a page that is used by a person. If the page is driven by a script with no human interaction, Turnstile usually will not hand out a token. The fix is not in the front end: get a token server side from a recognition service and inject it. It works the same way as hCaptcha: submit the sitekey and the page URL to the recognition endpoint, then put the token into cf-turnstile-response.
Most often the production domain was not added to the Turnstile allowed list, so the widget never passes and there is no token. The next most common cause is an extra cache or CDN layer in production that makes the submit happen before the callback.
No. Turnstile inserts that hidden field into its own form once verification passes. Just read it at the moment of submit, not when the page loads.
No. A token is single use and lives for about 300 seconds; submitting the same one twice fails.
To the back end they are the same thing: a submittable cf-turnstile-response. Only the way you obtained it differs.