SDKs

JavaScript

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.

@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 fetch and AbortController: 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 localhost during 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.

index.html
<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

  1. 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.

    fingerly.ts
    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 a ConsentError and reads nothing. If you already have consent, or rely on another lawful basis, load with consent: 'granted' instead. See privacy and consent and consent tools.

    Requests go to https://api.fingerly.io/api/v1/ingest unless you give the SDK a first-party proxy of your own. Loading collects nothing and sends nothing.

  2. Step 2: Identify at the moment that matters

    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.

    checkout.ts
    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 }),
      })
    }
    
  3. 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.

result
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.

deferred.ts
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.

local.ts
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.

proxy.ts
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.

errors.ts
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, 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.

Next steps