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

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.

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

  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'
    
    export const fingerly = await load({ apiKey: 'fly_pk_us_production_…' })
    

    The key decides where requests go: a fly_pk_us_… key talks to https://us.api.fingerly.io. 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 path on your own site with endpoints. The SDK appends /api/v1/identify to each base you give it.

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

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