Overview
After a user completes the Turnstile challenge, a token is generated. This token must be sent to your server for validation. There are multiple ways to retrieve this token.
Method 1: onSuccess Callback (Recommended)
The most common approach is using the onSuccess callback:
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
function MyForm () {
const [ token , setToken ] = useState < string >()
return (
< form >
< Turnstile
siteKey = "1x00000000000000000000AA"
onSuccess = { ( token ) => {
console . log ( 'Token received:' , token )
setToken ( token )
} }
/>
< button type = "submit" disabled = { ! token } >
Submit
</ button >
</ form >
)
}
This is the recommended approach as it’s reactive and works for all widget sizes and modes.
Turnstile automatically adds a hidden input field to the form:
import { Turnstile } from '@marsidev/react-turnstile'
function MyForm () {
const handleSubmit = ( e : React . FormEvent < HTMLFormElement >) => {
e . preventDefault ()
const formData = new FormData ( e . currentTarget )
const token = formData . get ( 'cf-turnstile-response' )
console . log ( 'Token from form:' , token )
}
return (
< form onSubmit = { handleSubmit } >
< input type = "email" name = "email" />
{ /* Hidden field "cf-turnstile-response" is auto-added */ }
< Turnstile
siteKey = "1x00000000000000000000AA"
options = { {
responseField: true , // default
responseFieldName: 'cf-turnstile-response' // default
} }
/>
< button type = "submit" > Submit </ button >
</ form >
)
}
Custom Field Name
You can customize the hidden field name:
< Turnstile
siteKey = "1x00000000000000000000AA"
options = { {
responseField: true ,
responseFieldName: 'turnstile-token'
} }
/>
Disable Hidden Field
If you don’t want the hidden field:
< Turnstile
siteKey = "1x00000000000000000000AA"
options = { {
responseField: false
} }
/>
Method 3: Using Refs (Synchronous)
Retrieve the token on demand using getResponse():
import { Turnstile , TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef } from 'react'
function MyForm () {
const turnstileRef = useRef < TurnstileInstance >( null )
const handleSubmit = () => {
const token = turnstileRef . current ?. getResponse ()
if ( token ) {
console . log ( 'Token:' , token )
// Submit to your API
} else {
console . log ( 'No token available yet' )
}
}
return (
< form >
< Turnstile
ref = { turnstileRef }
siteKey = "1x00000000000000000000AA"
/>
< button type = "button" onClick = { handleSubmit } >
Submit
</ button >
</ form >
)
}
getResponse() returns undefined if the widget hasn’t been solved yet.
Method 4: Using Refs (Asynchronous)
Wait for the token using getResponsePromise():
import { Turnstile , TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef } from 'react'
function MyForm () {
const turnstileRef = useRef < TurnstileInstance >( null )
const handleSubmit = async () => {
try {
// Wait up to 30 seconds for the token
const token = await turnstileRef . current ?. getResponsePromise ( 30000 )
console . log ( 'Token:' , token )
// Submit to your API
await fetch ( '/api/submit' , {
method: 'POST' ,
body: JSON . stringify ({ token })
})
} catch ( error ) {
console . error ( 'Failed to get token:' , error )
}
}
return (
< form >
< Turnstile
ref = { turnstileRef }
siteKey = "1x00000000000000000000AA"
/>
< button type = "button" onClick = { handleSubmit } >
Submit
</ button >
</ form >
)
}
Use getResponsePromise() for invisible widgets or when you need to ensure a token is available before proceeding.
For invisible widgets, combine execute() with getResponsePromise():
import { Turnstile , TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef } from 'react'
function InvisibleForm () {
const turnstileRef = useRef < TurnstileInstance >( null )
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ()
try {
// Trigger the invisible challenge
turnstileRef . current ?. execute ()
// Wait for the token
const token = await turnstileRef . current ?. getResponsePromise ()
// Submit with token
const response = await fetch ( '/api/submit' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ token })
})
if ( response . ok ) {
alert ( 'Success!' )
}
} catch ( error ) {
console . error ( 'Verification failed:' , error )
turnstileRef . current ?. reset ()
}
}
return (
< form onSubmit = { handleSubmit } >
< input type = "email" required />
< Turnstile
ref = { turnstileRef }
siteKey = "1x00000000000000000000AA"
options = { {
execution: 'execute' ,
size: 'invisible'
} }
/>
< button type = "submit" > Submit </ button >
</ form >
)
}
Complete Integration Example
Here’s a complete example combining multiple approaches:
import { Turnstile , TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef , useState } from 'react'
function CompleteForm () {
const turnstileRef = useRef < TurnstileInstance >( null )
const [ token , setToken ] = useState < string >()
const [ status , setStatus ] = useState < 'idle' | 'submitting' | 'success' | 'error' >( 'idle' )
const handleSubmit = async ( e : React . FormEvent < HTMLFormElement >) => {
e . preventDefault ()
// Method 1: Use token from state (via onSuccess)
let verificationToken = token
// Method 2: Fallback to ref if token not in state
if ( ! verificationToken ) {
verificationToken = turnstileRef . current ?. getResponse ()
}
// Method 3: Fallback to form data
if ( ! verificationToken ) {
const formData = new FormData ( e . currentTarget )
verificationToken = formData . get ( 'cf-turnstile-response' ) as string
}
if ( ! verificationToken ) {
alert ( 'Please complete the verification' )
return
}
// Check if token is expired
if ( turnstileRef . current ?. isExpired ()) {
alert ( 'Verification expired. Please try again.' )
turnstileRef . current ?. reset ()
return
}
setStatus ( 'submitting' )
try {
const response = await fetch ( '/api/submit' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
email: ( e . currentTarget . elements . namedItem ( 'email' ) as HTMLInputElement ). value ,
turnstileToken: verificationToken
})
})
if ( response . ok ) {
setStatus ( 'success' )
} else {
setStatus ( 'error' )
turnstileRef . current ?. reset ()
}
} catch ( error ) {
setStatus ( 'error' )
turnstileRef . current ?. reset ()
}
}
return (
< form onSubmit = { handleSubmit } >
< input
type = "email"
name = "email"
placeholder = "Email"
required
/>
< Turnstile
ref = { turnstileRef }
siteKey = "1x00000000000000000000AA"
onSuccess = { ( token ) => {
console . log ( 'Token received via callback:' , token )
setToken ( token )
} }
onExpire = { () => {
console . log ( 'Token expired' )
setToken ( undefined )
} }
onError = { () => {
console . log ( 'Error occurred' )
setToken ( undefined )
} }
/>
< button type = "submit" disabled = { ! token || status === 'submitting' } >
{ status === 'submitting' ? 'Submitting...' : 'Submit' }
</ button >
{ status === 'success' && < p > Form submitted successfully! </ p > }
{ status === 'error' && < p > Error submitting form. Please try again. </ p > }
</ form >
)
}
Method Comparison
Method When to Use Pros Cons onSuccessMost cases Reactive, immediate Requires state management Hidden Field Traditional forms No JavaScript needed Less flexible getResponse()On-demand retrieval Simple, direct Returns undefined if not ready getResponsePromise()Async workflows Waits for token Can timeout execute() + PromiseInvisible widgets Full control More complex
Best Practices
Check Expiration Always check if the token is expired before submitting
Handle Errors Reset the widget on submission errors
Use Callbacks Prefer onSuccess for reactive updates
Validate Server-Side Never trust client-side tokens alone
Next Steps
Server Validation Learn how to validate tokens on your server
Handling Expiration Manage token expiration
Interact with Widget Control widgets programmatically