[{"data":1,"prerenderedAt":193},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdevice-fingerprinting-react":3},{"page":4,"toc":169,"updated":192},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdevice-fingerprinting-react","Add device fingerprinting to a React app","React Device Fingerprinting: Add a Visitor ID with Hooks","Add device fingerprinting to a React app: wrap it in a provider, identify with a hook when the visitor acts, and verify the result on your server.",[10,13,18,24,27,95,98,100,105,109,112,117,120,124,126,129,131,134,136,139,141,144,146],{"type":11,"text":12},"p","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.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Before you start","before-you-start",{"type":19,"items":20},"list",[21,22,23],"React 19 or newer. Building with Next.js? Use the [Next.js tutorial](\u002Fdocs\u002Fdevice-fingerprinting-nextjs), 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 `consent` is `granted`.",{"type":14,"level":15,"text":25,"id":26},"Add the SDK and verify the result","add-the-sdk-and-verify-the-result",{"type":28,"steps":29},"steps",[30,37,53,64,77,90],{"title":31,"blocks":32},"Create your keys",[33,35],{"type":11,"text":34},"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.",{"type":11,"text":36},"Development keys run the same detection as production and cost nothing. See [API keys and environments](\u002Fdocs\u002Fapi-keys).",{"title":38,"blocks":39},"Install the SDK",[40],{"type":41,"samples":42},"code",[43,47,50],{"label":44,"lang":45,"code":46},"npm","bash","npm install @fingerly\u002Freact",{"label":48,"lang":45,"code":49},"pnpm","pnpm add @fingerly\u002Freact",{"label":51,"lang":45,"code":52},"yarn","yarn add @fingerly\u002Freact",{"title":54,"blocks":55},"Wrap your app in the provider",[56,58],{"type":11,"text":57},"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.",{"type":41,"samples":59},[60],{"label":61,"lang":62,"code":63},"main.tsx","tsx","import { createRoot } from 'react-dom\u002Fclient'\nimport { FingerlyProvider } from '@fingerly\u002Freact'\n\ncreateRoot(document.getElementById('root')!).render(\n  \u002F\u002F consent=\"granted\" only once you have consent (or another lawful basis)\n  \u003CFingerlyProvider apiKey=\"fly_pk_development_…\" consent=\"granted\">\n    \u003CApp \u002F>\n  \u003C\u002FFingerlyProvider>,\n)",{"title":65,"blocks":66},"Identify when the visitor acts",[67,69,75],{"type":11,"text":68},"`useIdentify` returns an `identify` function plus the shared state. Tag the identification with the action it protects, then send the request ID to your backend with the form.",{"type":41,"samples":70},[71],{"label":72,"lang":62,"code":73,"check":74},"SignUpForm.tsx","import { useIdentify } from '@fingerly\u002Freact'\n\nexport function SignUpForm() {\n  const { identify, isLoading, error } = useIdentify({ tag: 'signup' })\n\n  async function onSubmit(event: React.FormEvent\u003CHTMLFormElement>) {\n    event.preventDefault()\n    const email = new FormData(event.currentTarget).get('email')\n\n    let requestId: string | undefined\n    try {\n      ;({ requestId } = await identify())\n    } catch {\n      \u002F\u002F Carry on: your server treats a missing request ID as missing evidence.\n    }\n\n    await fetch('\u002Fapi\u002Fsignup', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\u002Fjson' },\n      body: JSON.stringify({ email, requestId }),\n    })\n  }\n\n  return (\n    \u003Cform onSubmit={onSubmit}>\n      \u003Cinput name=\"email\" type=\"email\" required \u002F>\n      \u003Cbutton disabled={isLoading}>{isLoading ? 'Checking…' : 'Create account'}\u003C\u002Fbutton>\n      {error && \u003Cp>{error.message}\u003C\u002Fp>}\n    \u003C\u002Fform>\n  )\n}",false,{"type":11,"text":76},"Use `useIdentify({ immediate: true })` instead to identify when a component mounts. It runs in an effect, so it never runs during server rendering.",{"title":78,"blocks":79},"Verify on your server",[80,82,88],{"type":11,"text":81},"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_level` and `visitor_id`.",{"type":41,"samples":83},[84],{"label":85,"lang":86,"code":87,"check":74},"Signup handler (Node.js)","ts","import { FingerlyAPIError, load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\n\nexport async function POST(request: Request) {\n  const { email, requestId } = await request.json()\n\n  let event = null\n  if (typeof requestId === 'string' && requestId !== '') {\n    try {\n      event = await fingerly.events.get(requestId)\n    } catch (error) {\n      if (!(error instanceof FingerlyAPIError && error.code === 'event_not_found')) throw error\n    }\n  }\n\n  const fresh = event && Date.now() - Date.parse(event.occurred_at) \u003C 2 * 60 * 1000\n  if (!event || event.tag !== 'signup' || !fresh) return Response.json({ status: 'verify' })\n  if (event.suspect_level === 'high') return Response.json({ status: 'review' })\n\n  await createAccount({ email, visitorId: event.visitor_id }) \u002F\u002F your own code\n  return Response.json({ status: 'created' })\n}",{"type":11,"text":89},"Other languages follow the same four checks. See [server-side verification](\u002Fdocs\u002Fserver-side-verification) for the Python, Go, Java, .NET, PHP, Ruby and Rust versions, and for the pending case.",{"title":91,"blocks":92},"Try it",[93],{"type":11,"text":94},"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.",{"type":14,"level":15,"text":96,"id":97},"Read the answer in the interface","read-the-answer-in-the-interface",{"type":11,"text":99},"`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.",{"type":41,"samples":101},[102],{"label":103,"lang":62,"code":104},"RiskNotice.tsx","import { useIdentify } from '@fingerly\u002Freact'\n\nfunction RiskNotice() {\n  const { suspectLevel } = useIdentify()\n  if (suspectLevel !== 'high') return null\n  return \u003Caside>We need one more check before you continue.\u003C\u002Faside>\n}",{"type":106,"tone":107,"text":108},"callout","warning","The score reaches the page as a convenience for trying the product. Do not use it for security decisions: make those on your server, from the stored event. See [suspect score](\u002Fdocs\u002Fsuspect-score#client-response).",{"type":14,"level":15,"text":110,"id":111},"Server rendering and frameworks","server-rendering-and-frameworks",{"type":19,"items":113},[114,115,116],"On the server the hooks return the unidentified state, so hydration always matches. Calling `identify()` during server rendering rejects with `FingerlyServerError`.","For a server that renders many requests, create a store per request with `createFingerlyStore(options)` and pass it as `\u003CFingerlyProvider store={store}>`.","Using Vue or Nuxt instead? See the [Vue and Nuxt tutorial](\u002Fdocs\u002Fdevice-fingerprinting-vue-nuxt).",{"type":14,"level":15,"text":118,"id":119},"Common questions","common-questions",{"type":14,"level":121,"text":122,"id":123},3,"How do I get a visitor ID in React?","how-do-i-get-a-visitor-id-in-react",{"type":11,"text":125},"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.",{"type":14,"level":121,"text":127,"id":128},"Does the SDK identify on every render?","does-the-sdk-identify-on-every-render",{"type":11,"text":130},"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.",{"type":14,"level":121,"text":132,"id":133},"Can I use it with a custom consent banner?","can-i-use-it-with-a-custom-consent-banner",{"type":11,"text":135},"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](\u002Fdocs\u002Fconsent-tools#frameworks).",{"type":14,"level":121,"text":137,"id":138},"Why is suspectLevel null?","why-is-suspectlevel-null",{"type":11,"text":140},"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.",{"type":14,"level":121,"text":142,"id":143},"Does it work inside a React Native or Capacitor web view?","does-it-work-inside-a-react-native-or-capacitor-web-view",{"type":11,"text":145},"No. Inside an app's web view the browser SDK collects nothing and the answer has `identifiable: false`. Use the [React Native tutorial](\u002Fdocs\u002Fdevice-fingerprinting-react-native) for apps.",{"type":147,"columns":15,"cards":148},"cards",[149,154,159,164],{"title":150,"text":151,"href":152,"logo":153},"React SDK","Every hook, option and error.","\u002Fdocs\u002Fsdks\u002Freact","react",{"title":155,"text":156,"href":157,"icon":158},"Server-side verification","The four checks before you trust a result.","\u002Fdocs\u002Fserver-side-verification","shield",{"title":160,"text":161,"href":162,"icon":163},"Fingerprinting vs cookies","When a cookie is enough and when it is not.","\u002Fdocs\u002Fdevice-fingerprinting-vs-cookies","layers",{"title":165,"text":166,"href":167,"icon":168},"Credential stuffing","Refuse automation at your login.","\u002Fdocs\u002Fuse-cases\u002Fcredential-stuffing","bot",[170,171,172,174,176,178,180,182,184,185,186,187,188,189,190,191],{"id":17,"text":16,"level":15},{"id":26,"text":25,"level":15},{"id":173,"text":31,"level":121},"step-create-your-keys",{"id":175,"text":38,"level":121},"step-install-the-sdk",{"id":177,"text":54,"level":121},"step-wrap-your-app-in-the-provider",{"id":179,"text":65,"level":121},"step-identify-when-the-visitor-acts",{"id":181,"text":78,"level":121},"step-verify-on-your-server",{"id":183,"text":91,"level":121},"step-try-it",{"id":97,"text":96,"level":15},{"id":111,"text":110,"level":15},{"id":119,"text":118,"level":15},{"id":123,"text":122,"level":121},{"id":128,"text":127,"level":121},{"id":133,"text":132,"level":121},{"id":138,"text":137,"level":121},{"id":143,"text":142,"level":121},"2026-11-12T11:59:51.000Z",1794493376687]