@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
The package also includes a minified script-tag build, dist/fingerly.global.js, which defines a global Fingerly object. Serve the file from your own domain and load it before your code.
<script src="/assets/fingerly.global.js"></script>
<script>
Fingerly.load({ apiKey: 'fly_pk_us_production_…' })
.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' export const fingerly = await load({ apiKey: 'fly_pk_us_production_…' })The key decides where requests go: a
fly_pk_us_…key talks tohttps://us.api.fingerly.io. 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 path on your own site with endpoints. The SDK appends /api/v1/identify to each base you give it.
const fingerly = await load({
apiKey: 'fly_pk_us_production_…',
endpoints: '/metrics', // your proxy's path
fallbackToDefaultEndpoint: true, // optional: try the regional API last
})
The path needs a proxy behind it. See proxy integrations.
Handle errors
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 other than granted.
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.