Overview
Captcha protection is enabled once, at the organization level, in the Turnkey Dashboard. Once it’s on, every signup and every OTP send must carry a Cloudflare Turnstile token or the Auth Proxy rejects it.@turnkey/react-wallet-kit renders the widget and attaches tokens for you, so React web apps using it need no integration work. This page is for you if you’re building your own auth UI with @turnkey/core:
- Plain JavaScript or TypeScript web apps
- React web apps built directly on
@turnkey/core - Vue, Svelte, Angular, and other frameworks
Which requests are protected
Turnkey’s Auth Proxy enforces captcha on two endpoints (and their_v2 variants), and the token travels as an X-Captcha-Token header:
Everything else, including OTP verification and logins for accounts that already exist, is unprotected, so those calls never need a token.
Signup requests that already carry a verification token are exempt even on a protected endpoint, because the user passed captcha when the OTP was sent and the verification token is single-use.
Failure modes
Step 1: Fetch your site key
Turnkey provisions the Turnstile widget for you, so the site key comes from the Auth Proxy rather than your own Cloudflare account:turnstileSiteKey is present only when captcha is both enabled for your organization and released to it. When it’s undefined, skip the widget entirely and omit captchaToken everywhere. Requests behave exactly as before.
Call
getClientParams once during app initialization, alongside client.init(), and cache the result. There’s no need to re-fetch it per auth attempt.Captcha is in Early Access and is additionally gated per organization. If you’ve flipped the Dashboard toggle on but
getClientParams still returns no site key, your organization hasn’t been enabled for the feature yet, so reach out to Turnkey. The Auth Proxy won’t enforce captcha in this state either, so your auth flows keep working.Step 2: Render the widget
Turnkey configures the widget in Cloudflare’s Managed mode, so Turnstile decides per visitor whether an interactive challenge is needed. Render it withappearance: "interaction-only" so it stays hidden for the overwhelming majority of users and only appears when Cloudflare asks for interaction. This is what the React wallet kit does. Use the onBeforeInteractive callback to reveal a short prompt (“Let us know you’re human”) just before it appears, so the widget doesn’t materialize unexplained.
Mount the widget when your auth screen opens rather than when the user submits. Turnstile then solves in the background and a token is usually waiting by the time you need one.
Vanilla JavaScript
Add the Turnstile script:React (web)
@turnkey/react-wallet-kit uses @marsidev/react-turnstile internally, so it’s the wrapper we recommend for React apps built on core:
Step 3: Consume and pass tokens
Each token is single-use. After a request consumes one, clear your stored token and reset the widget so a fresh token is ready for the next request:{ captchaToken } or {}) lets you spread the result into SDK params, so the field is simply absent when captcha is disabled or no token is available.
Because a token may not have arrived yet at the moment the user taps, the React wallet kit polls for up to 5 seconds before giving up and sending the request without one. If you’d rather not wait, disable your auth buttons until a token exists and re-enable them from the widget’s success callback.
Passing tokens to SDK methods
Methods that accept captchaToken
Login-only methods (
verifyOtp, loginWithOtp, loginWithPasskey, loginWithOauth, and loginWithWallet) take no captcha token at all.
completeOtp accepts a captchaToken, but you don’t need to supply one. Email and phone signups that carry a verification token (which completeOtp obtains from verifyOtp) are exempt from the challenge, because the user already passed captcha when the code was sent. @turnkey/react-wallet-kit does not send a token here.OAuth redirects
OAuth signups are challenged, but the token is generated before the user leaves your app for the provider. Encode the captcha token into the OAuthstate parameter along with your other state (public key, session key, nonce), then read it back on return and pass it to completeOauth. This is what @turnkey/react-wallet-kit does for both its popup and redirect flows.
Important considerations
- One challenge per OTP flow. Only
initOtpneeds a token. Resending a code is anotherinitOtpcall, so it needs a fresh one too. - Deploy first, enable second. With no
turnstileSiteKeyreturned fromgetClientParams, the widget stays dormant andconsumeToken()returns{}, so it’s safe to ship ahead of the Dashboard toggle. - Reset after every use. Tokens are single-use; always reset the widget so the next one is pre-warmed.
- Handle expiration. Turnstile tokens expire after about five minutes. Clear your stored token on the expiry and error callbacks so you never submit a stale one.