[{"data":1,"prerenderedAt":213},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdevice-fingerprinting-nextjs":3},{"page":4,"toc":187,"updated":212},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdevice-fingerprinting-nextjs","Add device fingerprinting to a Next.js app","Next.js Device Fingerprinting: Server Actions Tutorial","Add device fingerprinting to a Next.js App Router app: get a visitor ID in a client component, then verify it in a Server Action with your secret key.",[10,13,18,24,27,102,105,107,115,117,120,122,125,130,133,137,139,142,144,147,149,152,154,157,159,162,164],{"type":11,"text":12},"p","This tutorial adds **device fingerprinting** to a Next.js app on the App Router. By the end, a sign-up form gets a visitor ID in the browser, passes the request ID to a Server Action, and the Server Action reads the stored result with a secret key before it creates the account. The same shape works for logins, checkouts and promotions.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Before you start","before-you-start",{"type":19,"items":20},"list",[21,22,23],"Next.js 15 or newer, with React 19. On the Pages Router the steps are the same, with the provider in `_app.tsx`.","A Fingerly account. New accounts start with credit, and development keys are free.","A decision about consent. The SDK collects nothing until its `consent` is `granted`. See [privacy and consent](\u002Fdocs\u002Fprivacy-and-consent).",{"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,62,73,84,97],{"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 packages",[40,42,55,60],{"type":11,"text":41},"`@fingerly\u002Fnext` is the browser side. `@fingerly\u002Fnode` reads events on the server.",{"type":43,"samples":44},"code",[45,49,52],{"label":46,"lang":47,"code":48},"npm","bash","npm install @fingerly\u002Fnext @fingerly\u002Fnode",{"label":50,"lang":47,"code":51},"pnpm","pnpm add @fingerly\u002Fnext @fingerly\u002Fnode",{"label":53,"lang":47,"code":54},"yarn","yarn add @fingerly\u002Fnext @fingerly\u002Fnode",{"type":43,"samples":56},[57],{"label":58,"lang":47,"code":59},".env.local","FINGERLY_API_KEY=fly_pk_development_…\nFINGERLY_SECRET_KEY=fly_sk_development_…",{"type":11,"text":61},"The public key is read from `process.env` on the server render and handed to the browser with the page, so it needs no `NEXT_PUBLIC_` prefix. The secret key never leaves your server.",{"title":63,"blocks":64},"Add the provider to your root layout",[65,67],{"type":11,"text":66},"The layout stays a Server Component. The provider is the client boundary, collects nothing when it mounts, and shares one identification across the page.",{"type":43,"samples":68},[69],{"label":70,"lang":71,"code":72},"app\u002Flayout.tsx","tsx","import { FingerlyProvider } from '@fingerly\u002Fnext'\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml>\n      \u003Cbody>\n        {\u002F* consent=\"granted\" only once you have consent (or another lawful basis) *\u002F}\n        \u003CFingerlyProvider apiKey={process.env.FINGERLY_API_KEY} consent=\"granted\">\n          {children}\n        \u003C\u002FFingerlyProvider>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  )\n}",{"title":74,"blocks":75},"Identify when the visitor acts",[76,78],{"type":11,"text":77},"Call `identify()` at the moment worth protecting, with a tag that names the action. The tag binds the result to this action, so your server can refuse a request ID that came from somewhere else.",{"type":43,"samples":79},[80],{"label":81,"lang":71,"code":82,"check":83},"app\u002Fsignup\u002FSignUpForm.tsx","'use client'\nimport { useIdentify } from '@fingerly\u002Fnext'\nimport { signUp } from '.\u002Factions'\n\nexport function SignUpForm() {\n  const { identify, isLoading } = useIdentify({ tag: 'signup' })\n\n  async function onSubmit(formData: FormData) {\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    await signUp(String(formData.get('email')), requestId)\n  }\n\n  return (\n    \u003Cform action={onSubmit}>\n      \u003Cinput name=\"email\" type=\"email\" required \u002F>\n      \u003Cbutton disabled={isLoading}>Create account\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  )\n}",false,{"title":85,"blocks":86},"Verify in the Server Action",[87,89,95],{"type":11,"text":88},"Read the stored event with the request ID, and check it exists, belongs to this action, and is recent. Then use `suspect_level` and `visitor_id` in your own rules.",{"type":43,"samples":90},[91],{"label":92,"lang":93,"code":94,"check":83},"app\u002Fsignup\u002Factions.ts","ts","'use server'\nimport { FingerlyAPIError, load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\n\nasync function readEvent(requestId: string) {\n  try {\n    return await fingerly.events.get(requestId)\n  } catch (error) {\n    \u002F\u002F Never issued in this environment, or past retention.\n    if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return null\n    throw error \u002F\u002F still pending, or unexpected: ask the visitor to retry, never refuse on it\n  }\n}\n\nexport async function signUp(email: string, requestId: string | undefined) {\n  const event = requestId ? await readEvent(requestId) : null\n\n  \u002F\u002F No usable identification is missing evidence, not proof of fraud.\n  if (!event || event.tag !== 'signup') return { status: 'verify' as const }\n  if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return { status: 'verify' as const }\n\n  if (event.suspect_level === 'high') return { status: 'review' as const }\n\n  await createAccount({ email, visitorId: event.visitor_id }) \u002F\u002F your own code\n  return { status: 'created' as const }\n}",{"type":11,"text":96},"An event that is not recorded yet comes back as pending, which is not a verdict. Wait the `Retry-After` the error carries and read again, and also check the request ID has not been used before. [Server-side verification](\u002Fdocs\u002Fserver-side-verification) explains each check, and the [use-case recipes](\u002Fdocs\u002Fuse-cases) include a helper that does all of them.",{"title":98,"blocks":99},"Try it",[100],{"type":11,"text":101},"Run the app, submit the form, then open **Identification > Events** in the dashboard. The identification is there with its visitor, score and signals. Now try a private window, a VPN or an automated browser, and watch the score change.",{"type":14,"level":15,"text":103,"id":104},"Carry the request ID in a hidden field instead","carry-the-request-id-in-a-hidden-field-instead",{"type":11,"text":106},"For a form that posts straight to a Server Action, `FingerlyRequestId` renders a hidden input that fills itself once the identification finishes, and `readRequestId` reads it back on the server.",{"type":43,"samples":108},[109,112],{"label":110,"lang":71,"code":111},"app\u002Fcontact\u002Fpage.tsx","import { FingerlyRequestId } from '@fingerly\u002Fnext'\nimport { submit } from '.\u002Factions'\n\nexport default function Page() {\n  return (\n    \u003Cform action={submit}>\n      \u003Cinput name=\"email\" \u002F>\n      \u003CFingerlyRequestId \u002F>\n      \u003Cbutton>Send\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  )\n}",{"label":113,"lang":93,"code":114},"app\u002Fcontact\u002Factions.ts","'use server'\nimport { readRequestId } from '@fingerly\u002Fnext\u002Fserver'\n\nexport async function submit(formData: FormData) {\n  const requestId = readRequestId(formData)\n  \u002F\u002F read the stored event for requestId with a secret key, then decide\n}",{"type":11,"text":116},"The field carries the page's one identification, so it is not bound to an action by a tag. Check the event is recent and unused, or call `identify` with a tag as above when the action needs it.",{"type":14,"level":15,"text":118,"id":119},"Get past content blockers","get-past-content-blockers",{"type":11,"text":121},"Content blockers refuse requests to third-party hosts. Serve the SDK's submissions from your own origin through a route handler, so they travel as ordinary first-party requests. The [Next.js SDK page](\u002Fdocs\u002Fsdks\u002Fnextjs#proxy-through-a-route-handler) has the route and the provider setting, and [proxy integrations](\u002Fdocs\u002Fproxy-integrations) explains the model.",{"type":14,"level":15,"text":123,"id":124},"Going to production","going-to-production",{"type":19,"items":126},[127,128,129],"Create **production** public and secret keys, with your real origins on the public key.","Identify only at the moments that carry risk or value. Each production identification is billed, and requests that could not identify a device are free. See [billing](\u002Fdocs\u002Fbilling).","Run in observe mode first: log what each level would have done before you enforce it. See [plan your integration](\u002Fdocs\u002Fplanning-your-integration).",{"type":14,"level":15,"text":131,"id":132},"Common questions","common-questions",{"type":14,"level":134,"text":135,"id":136},3,"Does device fingerprinting work with the Next.js App Router and the Pages Router?","does-device-fingerprinting-work-with-the-next-js-app-router-and-the-pages-router",{"type":11,"text":138},"Yes. `@fingerly\u002Fnext` supports both. On the App Router the provider goes in the root layout, which stays a Server Component. On the Pages Router, render the same provider in `_app.tsx`.",{"type":14,"level":134,"text":140,"id":141},"Can I fingerprint a visitor in a Server Component or on the server?","can-i-fingerprint-a-visitor-in-a-server-component-or-on-the-server",{"type":11,"text":143},"No. The identification happens in the browser. During a server render the hooks return the unidentified state so hydration always matches, and calling `identify()` there rejects with `FingerlyServerError`. Call it from an event handler or an effect.",{"type":14,"level":134,"text":145,"id":146},"Do I need a NEXT_PUBLIC variable for the key?","do-i-need-a-next-public-variable-for-the-key",{"type":11,"text":148},"Not if you pass the key to the provider as a prop, as above. A server render reads `process.env` at runtime, so any variable name works. The provider can also read `NEXT_PUBLIC_FINGERLY_API_KEY` itself.",{"type":14,"level":134,"text":150,"id":151},"Where does the secret key go?","where-does-the-secret-key-go",{"type":11,"text":153},"On your server only. Secret keys are refused when a request carries a browser `Origin` header, so they cannot be used from client code even by mistake.",{"type":14,"level":134,"text":155,"id":156},"What happens if identification fails?","what-happens-if-identification-fails",{"type":11,"text":158},"Let the visitor continue. `identify()` rejects with a `TransportError` or, until consent is granted, a `ConsentError`. Your server receives no request ID and treats that as missing evidence, for example by asking for verification, rather than blocking.",{"type":14,"level":134,"text":160,"id":161},"How do I stop ad blockers from interfering?","how-do-i-stop-ad-blockers-from-interfering",{"type":11,"text":163},"Serve the SDK from your own domain with a first-party proxy, mounted as a route handler. The path is random and unique to your proxy, so no rule written for another site matches it.",{"type":165,"columns":15,"cards":166},"cards",[167,172,177,182],{"title":168,"text":169,"href":170,"logo":171},"Next.js SDK","Every export, option and entry point.","\u002Fdocs\u002Fsdks\u002Fnextjs","nextjs",{"title":173,"text":174,"href":175,"icon":176},"Server-side verification","The four checks before you trust a result.","\u002Fdocs\u002Fserver-side-verification","shield",{"title":178,"text":179,"href":180,"icon":181},"Sign-up abuse","Limit accounts per device.","\u002Fdocs\u002Fuse-cases\u002Fsign-up-abuse","users",{"title":183,"text":184,"href":185,"icon":186},"What is device fingerprinting?","The idea, and what it cannot do.","\u002Fdocs\u002Fdevice-fingerprinting","fingerprint",[188,189,190,192,194,196,198,200,202,203,204,205,206,207,208,209,210,211],{"id":17,"text":16,"level":15},{"id":26,"text":25,"level":15},{"id":191,"text":31,"level":134},"step-create-your-keys",{"id":193,"text":38,"level":134},"step-install-the-packages",{"id":195,"text":63,"level":134},"step-add-the-provider-to-your-root-layout",{"id":197,"text":74,"level":134},"step-identify-when-the-visitor-acts",{"id":199,"text":85,"level":134},"step-verify-in-the-server-action",{"id":201,"text":98,"level":134},"step-try-it",{"id":104,"text":103,"level":15},{"id":119,"text":118,"level":15},{"id":124,"text":123,"level":15},{"id":132,"text":131,"level":15},{"id":136,"text":135,"level":134},{"id":141,"text":140,"level":134},{"id":146,"text":145,"level":134},{"id":151,"text":150,"level":134},{"id":156,"text":155,"level":134},{"id":161,"text":160,"level":134},"2026-11-12T11:59:51.000Z",1794493376681]