# JavaScript agent

> Every export, option, result field and error of the Fingerly browser SDK, @fingerly/web-js.

Last updated: 2026-09-17

This is the complete API of `@fingerly/web-js`. For a guided introduction, start with the [JavaScript SDK](https://docs.fingerly.io/docs/sdks/javascript).

```ts Usage
import { load } from '@fingerly/web-js'

const fingerly = await load({ apiKey: 'fly_pk_us_production_…' })
const result = await fingerly.identify({ tag: 'login' })
```

## Exports

| Export | Kind | Description |
| --- | --- | --- |
| `load` | Function | Creates a client. |
| `FingerlyClient` | Class | The client `load` returns. |
| `TransportError` | Class | The error `identify` rejects with. |
| `IdentifyOptions`, `IdentifyResult`, `IdentifyTrigger`, `Verdict`, `Verdicts`, `Confidence` | Types | Described below. |

The script-tag build, `dist/fingerly.global.js`, exposes the same members on `window.Fingerly`.

## load()

```ts Signature
function load(options: FingerlyOptions): Promise<FingerlyClient>
```

Creates a client. Loading collects and sends nothing. A configuration mistake throws synchronously, so call it with `await` inside `try`.

**Load options**

- `apiKey` (string, required): Your public key. Its prefix decides which regional API is used.
- `endpoints` (string | string[]): Ordered base URLs or root-relative paths to send requests to instead of the regional API, for a [proxy](https://docs.fingerly.io/docs/proxy-integrations). `/api/v1/identify` is appended to each. They are tried in order when one fails.
- `endpoint` (string): A single base URL or path. Prefer `endpoints`. Passing both throws.
- `fallbackToDefaultEndpoint` (boolean, default `false`): Try the key's regional API after the custom endpoints fail.
- `consent` ('granted' | 'pending' | 'denied', default `'granted'`): Whether the visitor has consented. Until it is `granted`, `identify()` and `collect()` collect and send nothing and reject with a `ConsentError`. See [consent](https://docs.fingerly.io/docs/privacy-and-consent#consent).
- `transport` (object): Replaces how requests are sent, for tests: an object whose `submit(submission, signal)` resolves with an [identify response](https://docs.fingerly.io/reference/identify#response), and optionally `submitSupplement(requestId, submission, signal)`. `endpoint`, `endpoints` and `fallbackToDefaultEndpoint` are then ignored. See [testing](https://docs.fingerly.io/docs/testing#replace-the-transport).
- `sources` (array): The signal sources to run. Every browser source by default; pass `[]` in tests to collect nothing.
- `schedule` (object): Collection tuning for `collect()` and for `identify()` with explicit `tiers`: `budgetMs`, `defaultSourceTimeoutMs` and `concurrency`.
  - `budgetMs` (number, default `1200`): The total collection budget.
  - `defaultSourceTimeoutMs` (number, default `300`): The time any one source may take.
  - `concurrency` (number, default `6`): Sources collected at once.

### Where requests go

| Key prefix | API |
| --- | --- |
| `fly_pk_us_…` | `https://us.api.fingerly.io` |
| `fly_pk_eu_…` | `https://eu.api.fingerly.io` (coming soon) |

`load` throws when `apiKey` is empty, when both `endpoint` and `endpoints` are set, when `endpoints` is empty, or when an endpoint is neither an absolute `http(s)` URL nor a path starting with `/`, or contains a query or fragment.

## identify()

```ts Signature
fingerly.identify(options?: IdentifyOptions): Promise<IdentifyResult>
```

Collects signals, submits them and resolves with the server's verdict. Each call is a new identification with its own idempotency key; the SDK does not cache results.

**IdentifyOptions**

- `tag` (string): Your own reference for this identification. Echoed on the event, in webhooks and in the dashboard.
- `submit` (boolean, default `true`): `false` collects and computes local verdicts without sending anything.
- `signal` (AbortSignal): Cancels collection and submission.
- `tiers` (Array<'fast' | 'deferred'>): Collect these tiers in one pass under `schedule.budgetMs`, with no separate deferred report. Omit for the default: a 300 ms initial tier, then a 1,200 ms deferred tier after the answer.

## IdentifyResult

- `requestId` (string): The identification's ID. Empty when not submitted.
- `visitorId` (string): The stable visitor identifier. Empty only when not submitted or on a duplicate.
- `visitorIsNew` (boolean): Whether your organization is seeing this visitor for the first time.
- `visitorConfidence` (number): 0 to 100: `100` for an exact match, `85` to `99` when recognised after the device changed, `0` for a new visitor ID.
- `identifiable` (boolean): `false` when the browser gave too little to identify anyone. Still scored.
- `duplicate` (boolean): `true` when the server had already answered this request. Keep the first response.
- `state` (string): `enriched`, or `unavailable` when the network lookup could not run. Empty when not submitted.
- `suspectScore` (number | null): The weighted sum of the signals that fired. `null` when not scored, which is not `0`. Not a percentage and not capped.
- `suspectLevel` (string | null): `low`, `medium` or `high`. `null` exactly when `suspectScore` is.
- `triggers` (IdentifyTrigger[]): The signals that fired, heaviest first.
  - `signal` (string): The signal.
  - `group` (string): Its group.
  - `weight` (number): The weight it added.
  - `confidence` (string): `low`, `medium` or `high`.
- `verdicts` (Verdicts): Local verdicts from the initial tier. Advisory.
- `report` (SignalReport): The report that was sent.
- `deferred` (Promise<DeferredIdentifyResult>): The deferred tier's outcome. Never rejects.

## Verdicts

| Key | Matches when |
| --- | --- |
| `incognito` | The page is in a private browsing window. |
| `shields` | Anti-fingerprinting protections are rewriting values. A privacy choice, not a reason to challenge anyone. |
| `tor` | The browser is Tor Browser. |
| `emulator` | The browser runs in an emulated device. |
| `automation` | The browser is driven by automation. |
| `farm` | The browser looks mass-provisioned or reset fresh. |

**Verdict**

- `value` (boolean): Whether the verdict matched.
- `confidence` ('low' | 'medium' | 'high'): How strong the evidence is.
- `reasons` (string[]): Short, stable tokens for the evidence, strongest first.

> **Note:** Verdicts in the browser can be edited by whoever controls the browser. The server runs its own detection over the stored report, and its score is the one to act on. See [client-side verdicts](https://docs.fingerly.io/docs/client-verdicts).

## DeferredIdentifyResult

| `status` | Fields | When |
| --- | --- | --- |
| `'submitted'` | `report`, `combinedReport`, `verdicts` | The deferred report was sent. `verdicts` covers both tiers. |
| `'skipped'` | `reason`: `'not-submitted'`, `'explicit-tiers'`, `'duplicate'` | No deferred report applied. |
| `'failed'` | `error`, and whatever was collected | Collection or sending failed. The identification itself is unaffected. |

## collect()

```ts Signature
fingerly.collect(options?: IdentifyOptions): Promise<SignalReport>
```

Runs collection and returns the report without submitting anything. Uses `tiers` (both by default), `signal` and `schedule.budgetMs`.

## setConsent()

```ts Signature
fingerly.setConsent(state: 'granted' | 'pending' | 'denied'): void
fingerly.consent: 'granted' | 'pending' | 'denied'
fingerly.onConsentChange(listener: (state) => void): () => void
```

Changes the consent state. It applies to the next call and to any call already running: withdrawing consent stops collection and sending, and a running deferred tier resolves as `skipped` with the reason `consent-withdrawn`. `onConsentChange` calls its listener on every change and returns a function that unsubscribes. Any other value throws a `TypeError`.

## ConsentError

```ts Definition
declare class ConsentError extends Error {
  readonly name: 'ConsentError'
  readonly code: 'consent_required'
  readonly state: 'pending' | 'denied'
}
```

`identify()` and `collect()` reject with it when consent is not `granted`, or is withdrawn while they run.

## TransportError

```ts Definition
declare class TransportError extends Error {
  readonly name: 'TransportError'
  readonly status?: number      // the HTTP status, when the server answered
  readonly retryable: boolean   // whether trying again later may succeed
}
```

| Situation | `status` | `retryable` |
| --- | --- | --- |
| The server refused the request | The HTTP status | `true` for `404`, `405`, `408`, `425`, `429` and `5xx` |
| The network failed or an attempt timed out | none | `true` |
| The request was aborted | none | `false` |
| A `2xx` without a request ID, such as a captive portal page | none | `false` |

`identify` rejects only after its own retries: up to three attempts, 5 seconds each, with jittered backoff and one idempotency key.

## Runtime

- ES2020, using `fetch` and `AbortController`. No workers, WebAssembly or `eval` are required.
- Requests omit credentials and set no cookies. The SDK stores no identifier on the device.
- About 18 KB gzipped for the script-tag build.
