[{"data":1,"prerenderedAt":207},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fsdks\u002Fjavascript":3},{"page":4,"toc":188,"updated":206},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fsdks\u002Fjavascript","JavaScript","JavaScript SDK: Identify Browser Visitors","Install the browser SDK, identify a visitor at the moments that matter, and send the request ID to your server. Works with any framework, or none.",[10,13,17,22,28,31,44,46,50,52,58,61,94,97,99,104,106,109,111,114,116,121,123,126,128,133,136,138,143,145,148,150,155,160,163,166],{"type":11,"text":12},"p","`@fingerly\u002Fweb-js` is the browser SDK every web framework binding is built on. It collects device signals within a strict time budget, submits them with your public key, and resolves with the server's answer: the visitor ID, the suspect score and the signals behind it, in one round trip.",{"type":14,"tone":15,"text":16},"callout","note","Use a framework binding if you build with [React](\u002Fdocs\u002Fsdks\u002Freact), [Next.js](\u002Fdocs\u002Fsdks\u002Fnextjs), [Vue](\u002Fdocs\u002Fsdks\u002Fvue), [Nuxt](\u002Fdocs\u002Fsdks\u002Fnuxt), [Svelte](\u002Fdocs\u002Fsdks\u002Fsvelte) or [Angular](\u002Fdocs\u002Fsdks\u002Fangular). They share one identification across the page, handle server rendering, and expose the same result as reactive state.",{"type":18,"level":19,"text":20,"id":21},"heading",2,"Requirements","requirements",{"type":23,"items":24},"list",[25,26,27],"A browser that runs ES2020 with `fetch` and `AbortController`: every current version of Chrome, Edge, Firefox and Safari.","A [public key](\u002Fdocs\u002Fapi-keys) whose allowed origins include the site the SDK runs on.","A secure context (HTTPS, or `localhost` during development). The SDK still runs without one, but several signals are only available to secure pages.",{"type":18,"level":19,"text":29,"id":30},"Install","install",{"type":32,"samples":33},"code",[34,38,41],{"label":35,"lang":36,"code":37},"npm","bash","npm install @fingerly\u002Fweb-js",{"label":39,"lang":36,"code":40},"pnpm","pnpm add @fingerly\u002Fweb-js",{"label":42,"lang":36,"code":43},"yarn","yarn add @fingerly\u002Fweb-js",{"type":11,"text":45},"The package ships ES module and CommonJS builds with TypeScript declarations. It has no dependencies of its own at runtime.",{"type":18,"level":47,"text":48,"id":49},3,"Script tag","script-tag",{"type":11,"text":51},"The package also includes a minified script-tag build, `dist\u002Ffingerly.global.js`, which defines a global `Fingerly` object. Serve the file from your own domain and load it before your code.",{"type":32,"samples":53},[54],{"label":55,"lang":56,"code":57},"index.html","html","\u003Cscript src=\"\u002Fassets\u002Ffingerly.global.js\">\u003C\u002Fscript>\n\u003Cscript>\n  Fingerly.load({ apiKey: 'fly_pk_us_production_…' })\n    .then((fingerly) => fingerly.identify({ tag: 'login' }))\n    .then((result) => console.log(result.requestId))\n\u003C\u002Fscript>",{"type":18,"level":19,"text":59,"id":60},"Identify a visitor","identify-a-visitor",{"type":62,"steps":63},"steps",[64,77,87],{"title":65,"blocks":66},"Load the agent once",[67,69,75],{"type":11,"text":68},"Call `load()` with your public key when your application starts. Keep the returned client and reuse it for every identification.",{"type":32,"samples":70},[71],{"label":72,"lang":73,"code":74},"fingerly.ts","ts","import { load } from '@fingerly\u002Fweb-js'\n\nexport const fingerly = await load({ apiKey: 'fly_pk_us_production_…' })",{"type":11,"text":76},"The key decides where requests go: a `fly_pk_us_…` key talks to `https:\u002F\u002Fus.api.fingerly.io`. Loading collects nothing and sends nothing.",{"title":78,"blocks":79},"Identify at the moment that matters",[80,82],{"type":11,"text":81},"Call `identify()` when the visitor does something worth protecting: signing up, logging in, checking out. Pass a `tag` that names the action, so your server can check the identification belongs to it.",{"type":32,"samples":83},[84],{"label":85,"lang":73,"code":86},"checkout.ts","import { fingerly } from '.\u002Ffingerly'\n\nasync function onCheckout(orderId: string) {\n  const { requestId } = await fingerly.identify({ tag: 'checkout:' + orderId })\n\n  await fetch('\u002Fapi\u002Fcheckout', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ orderId, requestId }),\n  })\n}",{"title":88,"blocks":89},"Decide on your server",[90,92],{"type":11,"text":91},"Your backend reads the stored event by its request ID with a secret key, checks the tag and the time, and makes the decision. A result the browser reports can be edited by whoever controls the browser; the stored event cannot.",{"type":11,"text":93},"Continue with [server-side verification](\u002Fdocs\u002Fserver-side-verification).",{"type":18,"level":19,"text":95,"id":96},"What identify returns","what-identify-returns",{"type":11,"text":98},"`identify()` resolves once the server has answered, typically within a few hundred milliseconds. The initial collection is capped at 300 ms.",{"type":32,"samples":100},[101],{"label":102,"lang":73,"code":103},"result","const result = await fingerly.identify({ tag: 'login' })\n\nresult.requestId          \u002F\u002F '01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4'\nresult.visitorId          \u002F\u002F 'X9pL2mRc7KvT4bQw8NdF'\nresult.visitorIsNew       \u002F\u002F false\nresult.visitorConfidence  \u002F\u002F 100\nresult.identifiable       \u002F\u002F true\nresult.suspectScore       \u002F\u002F 37\nresult.suspectLevel       \u002F\u002F 'high'\nresult.triggers           \u002F\u002F [{ signal: 'tor', group: 'tor', weight: 14, confidence: 'high' }, …]\nresult.verdicts           \u002F\u002F local, advisory verdicts\nresult.deferred           \u002F\u002F a promise for the second collection tier",{"type":11,"text":105},"Every field is described in the [JavaScript agent reference](\u002Freference\u002Fjavascript-agent#identifyresult).",{"type":18,"level":19,"text":107,"id":108},"Tags","tags",{"type":11,"text":110},"A tag is your own reference for an identification, echoed back on the stored event, in webhooks and in the dashboard. Use it to bind an identification to the action it was made for, such as `checkout:8412` or `login`, and compare it on your server before you trust the result.",{"type":18,"level":19,"text":112,"id":113},"The deferred tier","the-deferred-tier",{"type":11,"text":115},"Some signals take longer to read than the 300 ms initial budget allows. After the server has answered, the SDK keeps collecting them for up to 1,200 ms more and sends them as a deferred report attached to the same request. The deferred report is archived with the event. It never changes the visitor ID, the score, what you are charged or which webhooks fire.",{"type":32,"samples":117},[118],{"label":119,"lang":73,"code":120},"deferred.ts","const result = await fingerly.identify({ tag: 'login' })\n\n\u002F\u002F Optional: observe the second tier when it finishes.\nconst deferred = await result.deferred\nif (deferred.status === 'submitted') {\n  console.log(deferred.verdicts)   \u002F\u002F verdicts over both tiers\n}",{"type":11,"text":122},"`result.deferred` never rejects. It resolves with `status: 'submitted'`, `'skipped'` or `'failed'`.",{"type":18,"level":19,"text":124,"id":125},"Collect without sending","collect-without-sending",{"type":11,"text":127},"Pass `submit: false` to collect and compute local verdicts without contacting the server. Nothing is scored, so `suspectScore` is `null` and `requestId` is empty. `collect()` returns just the report.",{"type":32,"samples":129},[130],{"label":131,"lang":73,"code":132},"local.ts","const local = await fingerly.identify({ submit: false })\nlocal.suspectScore   \u002F\u002F null: nobody scored it\nlocal.verdicts       \u002F\u002F computed in the browser\n\nconst report = await fingerly.collect()",{"type":18,"level":19,"text":134,"id":135},"Serve from your own domain","serve-from-your-own-domain",{"type":11,"text":137},"Content blockers often refuse requests to third-party API hosts. Route the SDK through a path on your own site with `endpoints`. The SDK appends `\u002Fapi\u002Fv1\u002Fidentify` to each base you give it.",{"type":32,"samples":139},[140],{"label":141,"lang":73,"code":142},"proxy.ts","const fingerly = await load({\n  apiKey: 'fly_pk_us_production_…',\n  endpoints: '\u002Fmetrics',                 \u002F\u002F your proxy's path\n  fallbackToDefaultEndpoint: true,       \u002F\u002F optional: try the regional API last\n})",{"type":11,"text":144},"The path needs a proxy behind it. See [proxy integrations](\u002Fdocs\u002Fproxy-integrations).",{"type":18,"level":19,"text":146,"id":147},"Handle errors","handle-errors",{"type":11,"text":149},"`identify()` rejects with a `TransportError` when the request cannot be completed. Branch on `status` and `retryable`. It rejects with a `ConsentError` when you loaded the SDK with a [consent state](\u002Fdocs\u002Fprivacy-and-consent#consent) other than `granted`.",{"type":32,"samples":151},[152],{"label":153,"lang":73,"code":154},"errors.ts","import { TransportError } from '@fingerly\u002Fweb-js'\n\ntry {\n  await fingerly.identify({ tag: 'signup' })\n} catch (error) {\n  if (error instanceof TransportError) {\n    if (error.status === 401) {\n      \u002F\u002F Wrong key, or this origin is not in the key's allowed origins.\n    } else if (error.status === 402) {\n      \u002F\u002F The organization is not accepting traffic: add funds.\n    } else if (error.retryable) {\n      \u002F\u002F Network trouble that outlasted the SDK's own retries.\n    }\n  }\n  \u002F\u002F Let the visitor continue: decide on the server without a request ID.\n}",{"type":23,"items":156},[157,158,159],"The SDK already retries network failures, timeouts, `429` and `5xx` up to three attempts, with jittered backoff and one idempotency key, so a retried request is answered and charged once.","Each attempt times out after 5 seconds.","`load()` throws synchronously for a configuration mistake, such as a missing `apiKey` or passing both `endpoint` and `endpoints`. Await it inside `try`.",{"type":14,"tone":161,"text":162},"warning","Never block a visitor only because identification failed. Treat a missing request ID as missing evidence, and let your server decide what that means for the action.",{"type":18,"level":19,"text":164,"id":165},"Next steps","next-steps",{"type":167,"columns":19,"cards":168},"cards",[169,173,178,183],{"title":170,"text":171,"href":172,"icon":32},"JavaScript agent reference","Every option, field and error.","\u002Freference\u002Fjavascript-agent",{"title":174,"text":175,"href":176,"icon":177},"Server-side verification","Read the result with a secret key.","\u002Fdocs\u002Fserver-side-verification","server",{"title":179,"text":180,"href":181,"icon":182},"Client-side verdicts","What the local verdicts mean.","\u002Fdocs\u002Fclient-verdicts","shield",{"title":184,"text":185,"href":186,"icon":187},"Content Security Policy","What to allow for the SDK.","\u002Fdocs\u002Fcontent-security-policy","lock",[189,190,191,192,193,195,197,199,200,201,202,203,204,205],{"id":21,"text":20,"level":19},{"id":30,"text":29,"level":19},{"id":49,"text":48,"level":47},{"id":60,"text":59,"level":19},{"id":194,"text":65,"level":47},"step-load-the-agent-once",{"id":196,"text":78,"level":47},"step-identify-at-the-moment-that-matters",{"id":198,"text":88,"level":47},"step-decide-on-your-server",{"id":96,"text":95,"level":19},{"id":108,"text":107,"level":19},{"id":113,"text":112,"level":19},{"id":125,"text":124,"level":19},{"id":135,"text":134,"level":19},{"id":147,"text":146,"level":19},{"id":165,"text":164,"level":19},"2026-09-17T16:57:29.000Z",1789667797781]