Client integration
Handle success, expiry, errors, and accessibility
Keep the form usable while tokens expire, widgets fail, users navigate by keyboard, and networks block a challenge resource.
8 minute lesson
A successful callback provides a token, but tokens expire and are single-use. A token lives about five minutes. If someone opens your form, gets distracted, and submits later, the token in the hidden field is already dead and Siteverify will reject it.
Wire up the lifecycle callbacks so your page reacts instead of failing silently:
<div
class="cf-turnstile"
data-sitekey="0x4AAAAAAABkMYinukE8nzKd"
data-callback="onToken"
data-expired-callback="onExpired"
data-error-callback="onWidgetError"
></div>
function onToken(token) {
document.querySelector('#submit').disabled = false
}
function onExpired() {
turnstile.reset()
}
function onWidgetError() {
showRetryMessage('The verification failed to load. Retry below.')
}
turnstile.reset() discards the stale token and runs the widget again. Reset or obtain a fresh token after expiry, after a failed server validation, and after a completed attempt. A token that already passed Siteverify once cannot be spent again, so a second submit needs a new one.
Keep the form recoverable
Do not leave the submit button permanently disabled after an error. Provide a clear retry state and preserve user-entered form data. Nothing burns trust like a form that eats a long message because a challenge script failed.
The error callback also fires when the network blocks challenges.cloudflare.com. Corporate proxies and aggressive content blockers do this. Show a message that explains what to do next, not a silently dead button.
Accessibility is still your job
Turnstile does not replace accessible form labels and errors. Test keyboard navigation, zoom, screen-reader output, slow networks, and script-blocking behavior. Tab through the whole form and confirm focus lands somewhere sensible after a widget reset.
Now rehearse the failure. Fill the form, force expiry by calling turnstile.reset() from the console (or wait out the five minutes), then submit. Verify the user can recover without retyping the message: the error is announced, the fields keep their values, and a fresh token lets the second attempt succeed.
Lesson completed