This tutorial adds device fingerprinting to a React 19 app with the Fingerly React SDK. A provider makes one identification per page, a hook asks for it when the visitor acts, and your server reads the stored result before it decides. It works in any React app, and the example protects a sign-up.
Before you start
- React 19 or newer. Building with Next.js? Use the Next.js tutorial, which adds Server Action helpers.
- A public key with your app's origin in its allowed origins, and a secret key for your server.
- A way to decide on consent: the SDK collects nothing until
consentisgranted.
Add the SDK and verify the result
Step 1: Create your keys
In the Fingerly dashboard, open Integration > SDK keys and create a public key in the development environment. Add the origin your app runs on as an allowed origin. A public key with no allowed origins refuses every browser request. Then create a secret key in the same environment for your server.
Development keys run the same detection as production and cost nothing. See API keys and environments.
Step 2: Install the SDK
npm install @fingerly/reactStep 3: Wrap your app in the provider
The provider collects nothing when it mounts. However many components ask for an identification, the page makes one and every component reads the same answer.
import { createRoot } from 'react-dom/client' import { FingerlyProvider } from '@fingerly/react' createRoot(document.getElementById('root')!).render( // consent="granted" only once you have consent (or another lawful basis) <FingerlyProvider apiKey="fly_pk_development_…" consent="granted"> <App /> </FingerlyProvider>, )Step 4: Identify when the visitor acts
useIdentifyreturns anidentifyfunction plus the shared state. Tag the identification with the action it protects, then send the request ID to your backend with the form.import { useIdentify } from '@fingerly/react' export function SignUpForm() { const { identify, isLoading, error } = useIdentify({ tag: 'signup' }) async function onSubmit(event: React.FormEvent<HTMLFormElement>) { event.preventDefault() const email = new FormData(event.currentTarget).get('email') let requestId: string | undefined try { ;({ requestId } = await identify()) } catch { // Carry on: your server treats a missing request ID as missing evidence. } await fetch('/api/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, requestId }), }) } return ( <form onSubmit={onSubmit}> <input name="email" type="email" required /> <button disabled={isLoading}>{isLoading ? 'Checking…' : 'Create account'}</button> {error && <p>{error.message}</p>} </form> ) }Use
useIdentify({ immediate: true })instead to identify when a component mounts. It runs in an effect, so it never runs during server rendering.Step 5: Verify on your server
The browser can be edited by whoever controls it, so the decision belongs on your backend. Read the stored event by request ID with your secret key, check it belongs to this action and is recent, and only then use
suspect_levelandvisitor_id.import { FingerlyAPIError, load } from '@fingerly/node' const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! }) export async function POST(request: Request) { const { email, requestId } = await request.json() let event = null if (typeof requestId === 'string' && requestId !== '') { try { event = await fingerly.events.get(requestId) } catch (error) { if (!(error instanceof FingerlyAPIError && error.code === 'event_not_found')) throw error } } const fresh = event && Date.now() - Date.parse(event.occurred_at) < 2 * 60 * 1000 if (!event || event.tag !== 'signup' || !fresh) return Response.json({ status: 'verify' }) if (event.suspect_level === 'high') return Response.json({ status: 'review' }) await createAccount({ email, visitorId: event.visitor_id }) // your own code return Response.json({ status: 'created' }) }Other languages follow the same four checks. See server-side verification for the Python, Go, Java, .NET, PHP, Ruby and Rust versions, and for the pending case.
Step 6: Try it
Submit the form, then open Identification > Events in the dashboard to see the visitor, the score and the signals. A private window, a VPN or an automated browser changes the score.
Read the answer in the interface
suspectLevel can adapt the interface, for example to show a second step. It is null when a request was not scored or your key hides it, so a check for high shows nothing then.
import { useIdentify } from '@fingerly/react'
function RiskNotice() {
const { suspectLevel } = useIdentify()
if (suspectLevel !== 'high') return null
return <aside>We need one more check before you continue.</aside>
}
Server rendering and frameworks
- On the server the hooks return the unidentified state, so hydration always matches. Calling
identify()during server rendering rejects withFingerlyServerError. - For a server that renders many requests, create a store per request with
createFingerlyStore(options)and pass it as<FingerlyProvider store={store}>. - Using Vue or Nuxt instead? See the Vue and Nuxt tutorial.
Common questions
How do I get a visitor ID in React?
Wrap the app in FingerlyProvider, call identify() from useIdentify() when the visitor acts, and read visitorId from the result. The same visitor ID is returned for the same device on its next visit, and your server should read it from the stored event rather than trust the page.
Does the SDK identify on every render?
No. The provider keeps one identification per page, and identify and refresh are stable across renders. Passing endpoints as an array literal does not restart anything either, because the array is compared by its contents.
Can I use it with a custom consent banner?
Yes. Load with the default pending state and call setConsent('granted') or setConsent('denied') from the store that useFingerly() returns when the visitor chooses. Changing the consent prop works too and does not remount the provider. See consent tools.
Why is suspectLevel null?
Because nothing was scored yet, the request was not scored, or your key hides the score from browsers. null is not a clean result and is not 0. Decide on your server.
Does it work inside a React Native or Capacitor web view?
No. Inside an app's web view the browser SDK collects nothing and the answer has identifiable: false. Use the React Native tutorial for apps.