Supports implicit (auto-render) and explicit (programmatic) rendering modes, plus React integration via @marsidev/react-turnstile
Mandatory server-side validation via Siteverify API; tokens expire in 5 minutes and are single-use only
Prevents 15 documented issues including CSP blocking, Safari Hide IP failures, Chrome/Edge first-load errors, token reuse bugs, and Jest incompatibility
Confirm successful installation by checking the skill directory location:
.cursor/skills/cloudflare-turnstile
Restart Cursor to activate cloudflare-turnstile. Access via /cloudflare-turnstile in your agent's command palette.
โ
Security Notice
We perform automated surface-level scans (Gen AI Scanner, Socket, Snyk) during installation. These checks detect common vulnerabilities but do not guarantee complete security. Always review skill source code and verify the publisher's reputation before production use.
Skills execute code in your environment. Always review source, verify the publisher, and test in isolation before production.
โ Call Siteverify API - Server-side validation is mandatory
โ Use HTTPS - Never validate over HTTP
โ Protect secret keys - Never expose in frontend code
โ Handle token expiration - Tokens expire after 5 minutes
โ Implement error callbacks - Handle failures gracefully
โ Use dummy keys for testing - Test sitekey: 1x00000000000000000000AA
โ Set reasonable timeouts - Don't wait indefinitely for validation
โ Validate action/hostname - Check additional fields when specified
โ Rotate keys periodically - Use dashboard or API to rotate secrets
โ Monitor analytics - Track solve rates and failures
โ Always pass client IP to Siteverify - Use CF-Connecting-IP header (Workers) or X-Forwarded-For (Node.js). Cloudflare briefly enforced strict remoteip validation in Jan 2025, causing widespread failures for sites not passing correct IP
Never Do
โ Skip server validation - Client-side only = security vulnerability
โ Proxy api.js script - Must load from Cloudflare CDN
โ Reuse tokens - Each token is single-use only
โ Use GET requests - Siteverify only accepts POST
โ Expose secret key - Keep secrets in backend environment only
โ Trust client-side validation - Tokens can be forged
โ Cache api.js - Future updates will break your integration
โ Use production keys in tests - Use dummy keys instead
โ Ignore error callbacks - Always handle failures
Known Issues Prevention
This skill prevents 15 documented issues:
Issue #1: Missing Server-Side Validation
Error: Zero token validation in Turnstile Analytics dashboard
Source: https://developers.cloudflare.com/turnstile/get-started/Why It Happens: Developers only implement client-side widget, skip Siteverify call
Prevention: All templates include mandatory server-side validation with Siteverify API
Error: Verification fails during success animation
Source: https://github.com/brave/brave-browser/issues/45608 (April 2025)
Why It Happens: Brave shields block animation scripts
Prevention: Templates handle success before animation completes
Issue #10: Next.js + Jest Incompatibility
Error: @marsidev/react-turnstile breaks Jest tests
Source: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025)
Why It Happens: Module resolution issues with Jest
Prevention: Testing guide includes Jest mocking patterns and dummy sitekey usage
Error: success: false with "token already spent" error
Source: https://developers.cloudflare.com/turnstile/troubleshooting/testingWhy It Happens: Each token can only be validated once. Turnstile tokens are single-use - after validation (success OR failure), the token is consumed and cannot be revalidated. Developers must explicitly call turnstile.reset() to generate a new token for subsequent submissions.
Prevention: Templates document single-use constraint and token refresh patterns
// CRITICAL: Reset widget after validation to get new tokenconst turnstileRef =useRef(null)asyncfunctionhandleSubmit(e){ e.preventDefault()const token = formData.get('cf-turnstile-response')const result =awaitfetch('/api/submit',{ method:'POST', body:JSON.stringify({ token })})// Reset widget regardless of success/failure// Token is consumed either wayif(turnstileRef.current){ turnstile.reset(turnstileRef.current)}}<Turnstile
ref={turnstileRef} siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken}/>