| name | token-lifecycle |
| description | Handle token generation, expiration, validation workflow, and form integration. Activate when implementing form submission with CAPTCHA, handling token expiration, or integrating with server-side validation.
|
| triggers | ["turnstile token expired","turnstile getResponse","turnstile reset","turnstile form submit","turnstile onSuccess","turnstile onExpire","validate turnstile token","turnstile server validation"] |
| category | lifecycle |
| metadata | {"library":"@marsidev/react-turnstile","library_version":"1.4.2","framework":"React"} |
Token Lifecycle
Handle token generation, expiration, validation workflow, and form integration.
Understanding Tokens
Turnstile tokens are single-use and expire after a timeout (typically 5 minutes). Once validated by your server, they cannot be used again.
Three Ways to Get Tokens
1. onSuccess Callback (Recommended)
Get the token when the user completes the challenge:
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export default function ContactForm() {
const [token, setToken] = useState<string | null>(null)
return (
<form>
<input type="email" placeholder="Email" />
<Turnstile
siteKey="YOUR_SITE_KEY"
onSuccess={(token) => setToken(token)}
/>
<button type="submit" disabled={!token}>
Submit
</button>
</form>
)
}
2. ref.current.getResponse()
Get the token imperatively at submission time:
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef } from 'react'
export default function ContactForm() {
const ref = useRef<TurnstileInstance>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const token = ref.current?.getResponse()
if (!token) {
alert('Please complete the CAPTCHA')
return
}
await submitForm(token)
}
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" />
<Turnstile ref={ref} siteKey="YOUR_SITE_KEY" />
<button type="submit">Submit</button>
</form>
)
}
3. Hidden Form Field
The widget automatically adds a hidden input. Access it via FormData:
export default function ContactForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const token = formData.get('cf-turnstile-response')
if (!token) {
alert('Please complete the CAPTCHA')
return
}
await submitForm(token)
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" />
<Turnstile siteKey="YOUR_SITE_KEY" />
<button type="submit">Submit</button>
</form>
)
}
Complete Form Integration
Best practice: Get token at submission time, validate server-side, reset widget:
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState } from 'react'
export default function ContactForm() {
const ref = useRef<TurnstileInstance>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setError(null)
const token = ref.current?.getResponse()
if (!token) {
setError('Please complete the CAPTCHA')
return
}
setIsSubmitting(true)
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, })
})
if (!response.ok) {
throw new Error('Submission failed')
}
ref.current?.reset()
} catch (err) {
setError('Failed to submit. Please try again.')
ref.current?.reset()
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<Turnstile
ref={ref}
siteKey="YOUR_SITE_KEY"
onExpire={() => {
// Optional: handle expiration
console.log('Token expired, please retry')
}}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
)
}
Handling Token Expiration
Tokens expire after ~5 minutes. Handle this with onExpire callback:
<Turnstile
ref={ref}
siteKey="YOUR_SITE_KEY"
options={{ refreshExpired: 'manual' }}
onExpire={() => {
alert('Verification expired. Please try again.')
ref.current?.reset()
}}
/>
Or let it auto-refresh:
<Turnstile
siteKey="YOUR_SITE_KEY"
options={{ refreshExpired: 'auto' }}
/>
Server-Side Validation
Important: The library provides TypeScript types but NO built-in validation. You must implement server-side validation yourself.
Example API Route (Next.js)
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { token } = await request.json()
const verification = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY!,
response: token,
}),
}
)
const result = await verification.json()
if (!result.success) {
return NextResponse.json(
{ error: 'CAPTCHA validation failed' },
{ status: 400 }
)
}
return NextResponse.json({ success: true })
}
Environment Variables
TURNSTILE_SECRET_KEY=0x0000000000000000000000000000000000000000000
Never expose the secret key client-side!
Imperative API Methods
Control the widget programmatically:
reset()
Reset the widget after submission or error:
ref.current?.reset()
getResponsePromise()
Wait for token with timeout (useful for programmatic flows):
try {
const token = await ref.current?.getResponsePromise(30000)
} catch (error) {
}
isExpired()
Check if token has expired:
if (ref.current?.isExpired()) {
ref.current?.reset()
}
remove() and render()
Advanced: Fully remove and re-render the widget:
ref.current?.remove()
ref.current?.render()
Common Mistakes
❌ Token Expires Before Form Submission
Problem: User takes too long to submit, token expires.
Wrong:
const [token, setToken] = useState<string | null>(null)
<Turnstile onSuccess={setToken} />
<button onClick={() => submit(token)}>Submit</button>
Correct:
const token = ref.current?.getResponse()
await submit(token)
ref.current?.reset()
❌ Expecting Built-in Server Validation
Wrong:
import { validateTurnstile } from '@marsidev/react-turnstile'
Correct:
const response = await fetch('/api/verify', {
method: 'POST',
body: JSON.stringify({ token })
})
❌ Calling Methods Before Widget Loads
Wrong:
useEffect(() => {
const token = ref.current?.getResponse()
}, [])
Correct:
<Turnstile
ref={ref}
onSuccess={(token) => {
}}
/>
❌ Not Resetting After Validation
Problem: Token is single-use. After server validation, it's invalid.
Correct:
const result = await validateToken(token)
if (result.success) {
ref.current?.reset()
}
Best Practices
- Get token at submission time - Not onSuccess callback
- Always reset after use - Tokens are single-use
- Handle expiration gracefully - Inform users and provide retry
- Validate server-side - Never trust client-side validation alone
- Keep secret key server-side - Never expose in client code
See Also