@fingerly/web-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.
Requirements
- A browser that runs ES2020 with
fetchandAbortController: every current version of Chrome, Edge, Firefox and Safari. - A public key whose allowed origins include the site the SDK runs on.
- A secure context (HTTPS, or
localhostduring development). The SDK still runs without one, but several signals are only available to secure pages.
Install
npm install @fingerly/web-js
The package ships ES module and CommonJS builds with TypeScript declarations. It has no dependencies of its own at runtime.
Script tag
Prefer the bundled import above: the SDK becomes part of your own code, with nothing separate for a blocker to refuse. For a page without a bundler, the package also includes a minified script-tag build, dist/browser.global.js, which defines a global Fingerly object. Serve the file from your own site under a neutral name, such as /assets/app-core.js, and load it before your code.
<script src="/assets/app-core.js"></script>
<script>
// consent: 'granted' only once you have consent (or another lawful basis)
Fingerly.load({ apiKey: 'fly_pk_production_…', consent: 'granted' })
.then((fingerly) => fingerly.identify({ tag: 'login' }))
.then((result) => console.log(result.requestId))
</script>
Identify a visitor
Step 1: Load the agent once
Call
load()with your public key when your application starts. Keep the returned client and reuse it for every identification.import { load } from '@fingerly/web-js' // Consent starts 'pending': nothing is collected until it is granted. export const fingerly = await load({ apiKey: 'fly_pk_production_…' }) // Call this from your consent banner or consent tool. export function onConsent(accepted: boolean) { fingerly.setConsent(accepted ? 'granted' : 'denied') }Until consent is
granted,identify()rejects with aConsentErrorand reads nothing. If you already have consent, or rely on another lawful basis, load withconsent: 'granted'instead. See privacy and consent and consent tools.Requests go to
https://api.fingerly.io/api/v1/ingestunless you give the SDK a first-party proxy of your own. Loading collects nothing and sends nothing.Step 2: Identify at the moment that matters
Call
identify()when the visitor does something worth protecting: signing up, logging in, checking out. Pass atagthat names the action, so your server can check the identification belongs to it.import { fingerly } from './fingerly' async function onCheckout(orderId: string) { const { requestId } = await fingerly.identify({ tag: 'checkout:' + orderId }) await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, requestId }), }) }Step 3: Decide on your server
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.
Continue with server-side verification.
What identify returns
identify() resolves once the server has answered, typically within a few hundred milliseconds. The initial collection is capped at 300 ms.
const result = await fingerly.identify({ tag: 'login' })
result.requestId // '01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4'
result.visitorId // 'X9pL2mRc7KvT4bQw8NdF'
result.visitorIsNew // false
result.visitorConfidence // 100
result.identifiable // true
result.suspectScore // 37
result.suspectLevel // 'high'
result.triggers // [{ signal: 'tor', group: 'tor', weight: 14, confidence: 'high' }, …]
result.verdicts // local, advisory verdicts
result.deferred // a promise for the second collection tier
Every field is described in the JavaScript agent reference.
Tags
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.
The deferred tier
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.
const result = await fingerly.identify({ tag: 'login' })
// Optional: observe the second tier when it finishes.
const deferred = await result.deferred
if (deferred.status === 'submitted') {
console.log(deferred.verdicts) // verdicts over both tiers
}
result.deferred never rejects. It resolves with status: 'submitted', 'skipped' or 'failed'.
Collect without sending
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.
const local = await fingerly.identify({ submit: false })
local.suspectScore // null: nobody scored it
local.verdicts // computed in the browser
const report = await fingerly.collect()
Serve from your own domain
Content blockers often refuse requests to third-party API hosts. Route the SDK through a proxy on your own site with endpoints, set to the path the dashboard issued for the proxy. The SDK posts both of its calls, the identification and the deferred report, to each entry exactly as given, with nothing appended.
const fingerly = await load({
apiKey: 'fly_pk_production_…',
endpoints: '/k7x2qm4a', // your proxy's issued path
fallbackToDefaultEndpoint: true, // optional: try the Fingerly API last
})
The path needs a proxy behind it. See proxy integrations. A hosted subdomain is given as its full URL, such as https://k7.shop.example/q3v8wz1p. An entry that is a bare origin, such as http://localhost:8099, is read as an API rather than a proxy and gets /api/v1/ingest. The deprecated endpoint option means the same as endpoints.
Handle errors
identify() rejects with a TransportError when the request cannot be completed. Branch on status and retryable. It rejects with a ConsentError while the consent state is not granted, which it is not until you grant it: pending is the default.
import { TransportError } from '@fingerly/web-js'
try {
await fingerly.identify({ tag: 'signup' })
} catch (error) {
if (error instanceof TransportError) {
if (error.status === 401) {
// Wrong key, or this origin is not in the key's allowed origins.
} else if (error.status === 402) {
// The organization is not accepting traffic: add funds.
} else if (error.retryable) {
// Network trouble that outlasted the SDK's own retries.
}
}
// Let the visitor continue: decide on the server without a request ID.
}
- The SDK already retries network failures, timeouts,
429and5xxup 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 missingapiKeyor passing bothendpointandendpoints. Await it insidetry.