# FingerprintIQ — Complete Documentation > Concatenated documentation for LLMs. Source: https://docs.fingerprintiq.com Generated: 2026-04-15 --- # Authentication > Authenticate your API requests with an API key. Source: https://docs.fingerprintiq.com/api/authentication ## API Keys All API requests require an API key passed via the `X-API-Key` header. ```bash curl -X POST https://fingerprintiq.com/v1/identify \ -H "Content-Type: application/json" \ -H "X-API-Key: fiq_live_your_api_key" \ -d '{"signals": {...}}' ``` ## Key Types Your API key. Two formats are supported: - **`fiq_live_`** — Live key for production traffic. Counts toward your monthly quota. - **`fiq_test_`** — Test key for development and staging. Full functionality, excluded from monthly quota. Use test keys (`fiq_test_`) in your CI/CD pipeline, staging environments, and local development. This keeps your production quota clean and makes it easy to distinguish production traffic in your dashboard. ## Creating API Keys Go to [fingerprintiq.com/dashboard](https://fingerprintiq.com/dashboard) and sign in. Select **API Keys** from the left sidebar. Click **Create Key**, give it a descriptive name (e.g., "Production Web App"), and select the key type. The full key value is shown only once at creation. Copy it to your secrets manager before closing the dialog. API keys are shown only once at creation time. If you lose a key, revoke it and create a new one — there is no way to retrieve the original value. ## CLI / AI Agent Authentication (Device Code Flow) If you're building a CLI, an AI agent integration, or any non-browser tool that needs to provision an API key on the user's behalf, use the device code flow instead of asking the user to copy/paste from the dashboard. It's modeled on OAuth 2.0 device authorization (RFC 8628). **How it works:** 1. Your tool asks FingerprintIQ for a device code. 2. Your tool prints a short verification URL to the user. 3. The user opens the URL, signs in, picks a project, and clicks **Authorize**. 4. Your tool polls for the key and receives it exactly once. The issued key is a standard `fiq_live_` key scoped to the project the user selected. It appears in Dashboard → API Keys and can be revoked there at any time. ### 1. Request a device code ```bash curl curl -X POST https://fingerprintiq.com/api/cli/device-code \ -H "Content-Type: application/json" \ -d '{"client_name": "my-cli"}' ``` ```javascript Node const res = await fetch("https://fingerprintiq.com/api/cli/device-code", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_name: "my-cli" }), }); const grant = await res.json(); ``` ```python Python import requests grant = requests.post( "https://fingerprintiq.com/api/cli/device-code", json={"client_name": "my-cli"}, ).json() ``` Human-readable name shown to the user on the consent screen (e.g. `claude-code`, `cursor`, `my-cli`). Capped at 64 characters. **Response:** ```json { "device_code": "…64-char opaque token…", "user_code": "ABCD-EFGH", "verification_url": "https://fingerprintiq.com/cli/auth?code=ABCD-EFGH", "expires_in": 600, "interval": 3 } ``` Opaque token your tool holds on to. Used to poll for the issued key. Never shown to the user. Short, human-readable code (8 chars, dashed). Shown to the user so they can confirm it matches what's on the consent screen. URL your tool should open (or print) for the user. Seconds until the grant expires. Always 600 (10 minutes). Minimum seconds between token poll requests. ### 2. Direct the user to authorize Print the `verification_url` and `user_code` and wait: ``` Open this URL in your browser to authorize: https://fingerprintiq.com/cli/auth?code=ABCD-EFGH Verification code: ABCD-EFGH Waiting for authorization… ``` On the consent page the user will sign in (if needed), pick which project the key should be scoped to, and confirm that the verification code matches. ### 3. Poll for the API key While waiting, poll `POST /api/cli/token` with the `device_code` every `interval` seconds: ```bash curl curl -X POST https://fingerprintiq.com/api/cli/token \ -H "Content-Type: application/json" \ -d '{"device_code": "…"}' ``` ```javascript Node async function pollForKey(deviceCode, interval) { while (true) { const res = await fetch("https://fingerprintiq.com/api/cli/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device_code: deviceCode }), }); const data = await res.json(); if (data.status === "authorized") return data.api_key; if (res.status === 410) throw new Error(`Grant ${data.status}`); await new Promise((r) => setTimeout(r, interval * 1000)); } } ``` ```python Python import time, requests def poll_for_key(device_code, interval): while True: r = requests.post( "https://fingerprintiq.com/api/cli/token", json={"device_code": device_code}, ) data = r.json() if data.get("status") == "authorized": return data["api_key"] if r.status_code == 410: raise RuntimeError(f"Grant {data.get('status')}") time.sleep(interval) ``` **Poll responses:** | Status | HTTP | Meaning | |--------|------|---------| | `{"status":"pending","interval":3}` | `200` | User hasn't authorized yet. Keep polling. | | `{"status":"authorized","api_key":"fiq_live_…"}` | `200` | Success. **Plaintext is returned exactly once** — save it immediately. | | `{"status":"expired"}` | `410` | 10-minute window elapsed. Start over from step 1. | | `{"status":"consumed"}` | `410` | Key was already returned on a prior poll. Start over. | The plaintext key is returned on a single successful poll and then cleared from the server. If your tool doesn't persist it on that first read, you'll need to start a new grant. ### 4. Use the key Store the key somewhere your tool can read it on future runs — typically an environment variable: ```bash export FINGERPRINTIQ_API_KEY='fiq_live_…' ``` Don't write the key to a shared or world-readable file without warning the user. The safest default is to export it for the current shell session and let the user add it to their `~/.zshrc`, `~/.bashrc`, or project `.env` themselves. The issued key is visible in Dashboard → API Keys under the name your tool supplied (e.g. `my-cli (CLI)`) and can be revoked there at any time. ## Rate Limits | Plan | Requests/second | Monthly limit | |------|-----------------|---------------| | Free | 10 | 10,000 | | Builder | 1,000 | 50,000 | | Growth | 1,000 | 250,000 | | Scale | 1,000 | 2,000,000 | When rate limited, the API returns `429 Too Many Requests` with a JSON body describing the limit: ```json { "error": "Monthly API call limit exceeded", "usage": 10001, "limit": 10000 } ``` The per-second rate limit applies to burst traffic. If your application generates identification requests in bursts (e.g., page load events), implement exponential backoff on 429 responses. ## Security Best Practices Never expose your API key in client-side JavaScript, public repositories, or build artifacts. Use environment variables on your server and `NEXT_PUBLIC_` / Vite `import.meta.env.VITE_` prefixes only for public keys used directly in the browser SDK. - **Server-side keys** — Use for calling the visits API (`/v1/demo/visits/:id`). These keys must never reach the browser. - **Client-side keys** — The SDK uses your key to call `/v1/identify`. This endpoint enforces domain allowlisting, so even if the key is visible in source, it cannot be used from unauthorized domains. - **Key rotation** — Rotate keys periodically (every 90 days) and immediately if you suspect exposure. --- # DELETE /v1/visitors/:visitorId > Permanently delete all data associated with a visitor for GDPR compliance. Source: https://docs.fingerprintiq.com/api/delete-visitor ## Endpoint ``` DELETE https://fingerprintiq.com/v1/visitors/:visitorId ``` This is a **Server API** endpoint. It requires a secret API key (`fiq_secret_*`) and must only be called from your backend, never from client-side code. This action is **irreversible**. Deleting a visitor permanently removes all associated events, visits, wallet connections, and the visitor record itself. The `visitorId` will no longer be recognized — future identifications from the same device will generate a new visitor record. ## Authentication Include your secret key in the Authorization header: ``` Authorization: Bearer fiq_secret_your_key_here ``` ## Path Parameters The visitor ID to delete. Format: `iq_` (e.g., `iq_01hns3k6tez83695a6t714s6n1`). ## What Gets Deleted Deleting a visitor removes all of the following from FingerprintIQ's systems: - The visitor record and stable `visitorId` - All identification events associated with the visitor - All visit history records - All linked wallet connections (Ethereum, Solana, and other chain addresses) - Any stored `linkedId` and `tag` metadata No data is retained in backups or cold storage after deletion. ```bash cURL curl -X DELETE https://fingerprintiq.com/v1/visitors/iq_01hns3k6tez83695a6t714s6n1 \ -H "Authorization: Bearer fiq_secret_your_key_here" ``` ```javascript Node.js const response = await fetch( 'https://fingerprintiq.com/v1/visitors/iq_01hns3k6tez83695a6t714s6n1', { method: 'DELETE', headers: { Authorization: 'Bearer fiq_secret_your_key_here', }, } ); const { deleted, eventsRemoved } = await response.json(); console.log(`Deleted visitor. Removed ${eventsRemoved} events.`); ``` ## Response ```json 200 OK { "deleted": true, "eventsRemoved": 42 } ``` ```json 404 Not Found { "error": "Visitor not found" } ``` ## Response Fields Always `true` when the deletion succeeds. Total count of identification events that were deleted as part of this operation. ## GDPR Compliance This endpoint is designed to fulfill GDPR Article 17 (Right to Erasure) and similar data deletion obligations. Upon receiving a deletion request from your users, call this endpoint with their associated `visitorId`. You are responsible for mapping your users to their `visitorId`(s) — use the `linkedId` field when creating events to make this mapping easy. If a user has multiple visitor records (e.g., they use different devices or browsers), you must call this endpoint once per `visitorId`. Use [GET /v1/events/search](/api/events-search) with `linkedId` to find all visitor IDs associated with a user before deleting. ## Error Responses | Status | Meaning | |--------|---------| | `401` | Missing or invalid secret API key | | `404` | Visitor not found or belongs to a different customer | --- # Error Reference > HTTP status codes and error responses across all FingerprintIQ API endpoints. Source: https://docs.fingerprintiq.com/api/errors All FingerprintIQ API errors return a JSON body with at minimum an `error` string. Some errors include additional fields such as `usage` and `limit` for rate-limit responses. ```json { "error": "Human-readable description of what went wrong" } ``` ## HTTP Status Codes ### 400 — Bad Request Returned when the request body or query parameters are invalid, malformed, or missing required fields. ```json 400 Bad Request (missing field) { "error": "Missing required field: 'signals'" } ``` ```json 400 Bad Request (invalid value) { "error": "Invalid query parameter: 'limit' must not exceed 100" } ``` ```json 400 Bad Request (malformed JSON) { "error": "Invalid JSON body" } ``` **Common causes:** - Sending an invalid JSON body to a `POST` or `PUT` endpoint - Omitting a required field (e.g., `signals` on `/v1/identify`) - Passing a `limit` value greater than 100 on search endpoints - Providing a `tag` object with nested values instead of a flat key-value structure --- ### 401 — Unauthorized Returned when the API key is missing, malformed, or does not match the required type for the endpoint. ```json 401 Unauthorized (missing key) { "error": "Missing API key" } ``` ```json 401 Unauthorized (invalid public key) { "error": "Invalid or missing API key" } ``` ```json 401 Unauthorized (wrong key type) { "error": "This endpoint requires a secret API key (fiq_secret_*). Public keys are not accepted." } ``` FingerprintIQ uses two key types. **Public keys** (`fiq_live_*`) are for the Browser API and JavaScript SDK — they are safe to embed in client-side code. **Secret keys** (`fiq_secret_*`) are for the Server API only and must never be exposed in the browser. Using a public key on a Server API endpoint will return a `401`. **Common causes:** - Forgetting the `Authorization: Bearer ...` header on Server API calls - Forgetting the `X-API-Key` header on Browser API calls - Using a public key on a server-only endpoint (or vice versa) - Providing a key from a different project or environment (e.g., staging key against production) --- ### 403 — Forbidden Returned when the API key is valid but the request is blocked by an access policy. ```json 403 Forbidden { "error": "Request origin not in allowlist" } ``` **Common causes:** - The requesting IP address is not in your project's IP allowlist - The request `Origin` header does not match a domain in your allowlist (Browser API only) - The API key has been restricted to specific endpoints and this request does not match --- ### 404 — Not Found Returned when a resource does not exist or belongs to a different customer's project. ```json 404 Not Found { "error": "Event not found" } ``` ```json 404 Not Found { "error": "Visitor not found" } ``` **Common causes:** - Querying an event `requestId` or visitor `visitorId` that does not exist - Querying a resource that belongs to a different project (FingerprintIQ returns `404` rather than `403` to avoid leaking existence information) - Accessing a deleted visitor after a [DELETE /v1/visitors/:visitorId](/api/delete-visitor) call --- ### 429 — Rate Limited Returned when you have exceeded a rate limit or your plan's monthly quota. ```json 429 Rate Limited (per-minute burst limit) { "error": "Rate limit exceeded. Retry after 60 seconds.", "usage": 120, "limit": 100 } ``` ```json 429 Rate Limited (monthly quota) { "error": "Monthly API call limit exceeded", "usage": 10001, "limit": 10000 } ``` Your current usage count — either requests in the current rate-limit window or total API calls this billing month. The applicable limit — either the per-window burst cap or your plan's monthly quota. **Common causes:** - High-frequency Server API polling (use webhooks instead) - Identifying many devices simultaneously during a traffic spike - Reaching your plan's monthly identification quota If you are regularly hitting monthly limits, [upgrade your plan](https://fingerprintiq.com/pricing) or contact support to discuss a custom quota. For burst-rate errors, implement exponential backoff (see [500 errors](#500--internal-server-error) below for a retry pattern). --- ### 500 — Internal Server Error Returned when FingerprintIQ's servers encounter an unexpected error. These are transient and should be retried. ```json 500 Internal Server Error { "error": "Internal server error" } ``` **Retry guidance:** Always retry `500` errors with exponential backoff and jitter to avoid amplifying load during incidents. A safe pattern: ```javascript JavaScript async function fetchWithRetry(url, options, maxAttempts = 4) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await fetch(url, options); if (response.status !== 500) return response; if (attempt < maxAttempts - 1) { const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 16000); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error(`Request failed after ${maxAttempts} attempts`); } ``` ```python Python import time, random, urllib.request def fetch_with_retry(url, headers, max_attempts=4): for attempt in range(max_attempts): req = urllib.request.Request(url, headers=headers) try: with urllib.request.urlopen(req) as res: if res.status != 500: return res except Exception: pass if attempt < max_attempts - 1: delay = min(1 * 2 ** attempt + random.random() * 0.5, 16) time.sleep(delay) raise RuntimeError(f"Request failed after {max_attempts} attempts") ``` --- ## Summary Table | Status | Name | Retryable | Description | |--------|------|-----------|-------------| | `200` | OK | — | Request succeeded | | `400` | Bad Request | No | Fix the request before retrying | | `401` | Unauthorized | No | Check your API key and key type | | `403` | Forbidden | No | Check your IP or domain allowlist | | `404` | Not Found | No | The requested resource does not exist | | `429` | Too Many Requests | Yes | Back off and retry after the indicated window | | `500` | Internal Server Error | Yes | Retry with exponential backoff | --- # GET /v1/events/search > Search and filter identification events. Source: https://docs.fingerprintiq.com/api/events-search ## Endpoint ``` GET https://fingerprintiq.com/v1/events/search ``` This is a **Server API** endpoint. It requires a secret API key (`fiq_secret_*`) and must only be called from your backend, never from client-side code. ## Authentication Include your secret key in the Authorization header: ``` Authorization: Bearer fiq_secret_your_key_here ``` ## Query Parameters Filter events to a specific visitor. Format: `iq_` (e.g., `iq_01hns3k6tez83695a6t714s6n1`). Filter events by customer-provided linked identifier (e.g., your internal user ID). Unix timestamp in milliseconds. Only return events at or after this time. Unix timestamp in milliseconds. Only return events at or before this time. When `true`, only return events manually flagged as suspect. When `false`, only return non-suspect events. Omit to return all events. Maximum number of events to return per page. Defaults to `20`, maximum is `100`. Opaque cursor from a previous response's `paginationKey` field. Pass this to retrieve the next page of results. ## Pagination This endpoint uses cursor-based pagination. When there are more results available, the response includes a `paginationKey` string. Pass it as the `paginationKey` query parameter in your next request to fetch the following page. When `paginationKey` is `null`, you have reached the last page. ```bash First page curl "https://fingerprintiq.com/v1/events/search?visitorId=iq_01hns3k6tez83695a6t714s6n1&limit=50" \ -H "Authorization: Bearer fiq_secret_your_key_here" ``` ```bash Next page (with cursor) curl "https://fingerprintiq.com/v1/events/search?visitorId=iq_01hns3k6tez83695a6t714s6n1&limit=50&paginationKey=eyJpZCI6InJlcV8wMWhuczR..." \ -H "Authorization: Bearer fiq_secret_your_key_here" ``` ```bash Filter by linkedId and time range curl "https://fingerprintiq.com/v1/events/search?linkedId=user_123&start=1712000000000&end=1712086400000" \ -H "Authorization: Bearer fiq_secret_your_key_here" ``` ## Response ```json 200 OK { "events": [ { "requestId": "req_01hns3k6tez83695a6t7", "visitorId": "iq_01hns3k6tez83695a6t714s6n1", "confidence": 0.97, "botProbability": 0.05, "linkedId": "user_123", "tag": { "page": "checkout" }, "suspect": false, "ip": "203.0.113.42", "url": "https://yoursite.com/checkout", "verdicts": { "bot": { "result": false, "probability": 0.05 }, "vpn": { "result": false, "confidence": 0.92 }, "tor": { "result": false }, "proxy": { "result": false }, "incognito": { "result": false }, "tampering": { "result": false, "anomalyScore": 0 }, "headless": { "result": false }, "virtualMachine": { "result": false }, "devtools": { "result": false }, "privacyBrowser": { "result": false, "name": null }, "highActivity": { "result": false }, "ipBlocklist": { "result": false }, "velocity": { "distinctIp": { "5m": 1, "1h": 1, "24h": 2 }, "distinctCountry": { "5m": 1, "1h": 1, "24h": 1 }, "events": { "5m": 1, "1h": 3, "24h": 15 } } }, "timestamp": 1712000003000, "createdAt": "2026-04-11T08:00:03.000Z" } ], "paginationKey": "eyJpZCI6InJlcV8wMWhuczR..." } ``` ```json 200 OK (last page) { "events": [...], "paginationKey": null } ``` ```json 400 Bad Request { "error": "Invalid query parameter: 'limit' must not exceed 100" } ``` ## Response Fields Array of event objects, ordered newest first. Each event contains the same fields as the [GET /v1/events/:requestId](/api/events) response, except that the full `signals` payload is omitted for performance. Fetch an individual event by `requestId` to retrieve complete signal data. Unique event identifier. Stable device identifier. Signal confidence score (0.0–1.0) for this event. Bot likelihood score (0.0–1.0) for this event. Customer-provided identifier, if set. Customer-provided metadata, if set. Whether this event was manually flagged as suspect. IP address at the time of the event. Page URL where identification occurred. All detection verdicts. See [GET /v1/events/:requestId](/api/events) for the full verdict schema. Unix timestamp (milliseconds) when the event occurred. ISO 8601 timestamp when the event was stored. Opaque cursor to retrieve the next page. Pass this value as the `paginationKey` query parameter in a subsequent request. `null` when there are no more results. ## Error Responses | Status | Meaning | |--------|---------| | `400` | Invalid query parameter value | | `401` | Missing or invalid secret API key | | `429` | Rate limit exceeded | --- # GET /v1/events/:requestId > Retrieve full event details including all signals. Source: https://docs.fingerprintiq.com/api/events ## Endpoint ``` GET https://fingerprintiq.com/v1/events/:requestId ``` This is a **Server API** endpoint. It requires a secret API key (`fiq_secret_*`) and must only be called from your backend, never from client-side code. ## Authentication Include your secret key in the Authorization header: ``` Authorization: Bearer fiq_secret_your_key_here ``` ## Path Parameters The `requestId` returned from the identify endpoint. ## Response Returns the full event with all client and server signals. ```json 200 OK { "requestId": "req_01hns3k6tez83695a6t7", "visitorId": "iq_01hns3k6tez83695a6t714s6n1", "confidence": 0.97, "botProbability": 0.05, "suspectScore": 0, "linkedId": "user_123", "tag": { "page": "checkout" }, "suspect": false, "ip": "203.0.113.42", "url": "https://yoursite.com/checkout", "referrer": "https://yoursite.com/", "verdicts": { "bot": { "result": false, "probability": 0.05 }, "vpn": { "result": false, "confidence": 0.92 }, "tor": { "result": false }, "proxy": { "result": false }, "incognito": { "result": false }, "tampering": { "result": false, "anomalyScore": 0 }, "headless": { "result": false }, "virtualMachine": { "result": false }, "devtools": { "result": false }, "privacyBrowser": { "result": false, "name": null }, "highActivity": { "result": false }, "ipBlocklist": { "result": false }, "velocity": { "distinctIp": { "5m": 1, "1h": 1, "24h": 2 }, "distinctCountry": { "5m": 1, "1h": 1, "24h": 1 }, "events": { "5m": 1, "1h": 3, "24h": 15 } } }, "signals": { "client": { "canvas": { "hash": "a3f2b1c4...", "isFarbled": false }, "webgl": { "renderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4)", "vendor": "Google Inc. (Apple)" }, "... all 41 client signals" }, "server": { "asn": { "asn": 7922, "org": "Comcast", "category": "RESIDENTIAL_ISP", "isDatacenter": false }, "geo": { "country": "US", "city": "New York", "rttCoherence": 1.0 }, "tls": { "cipher": "AEAD-AES256-GCM-SHA384", "version": "TLSv1.3", "ja4": "t13d..." }, "vpnDetection": { "verdict": "not_detected", "confidence": 0.92 }, "http": { "fingerprint": "...", "classification": "chrome" }, "... full server signals" } }, "timestamp": 1712000003000, "createdAt": "2026-04-11T08:00:03.000Z" } ``` ```json 404 Not Found { "error": "Event not found" } ``` ## Response Fields Unique event identifier. Stable device identifier. Signal confidence score from 0.0 to 1.0. Low values indicate that many signals failed to collect, which may itself indicate a bot or privacy tool. Bot likelihood score from 0.0 to 1.0. Combines datacenter ASN, headless markers, software renderer, API tampering, and UA/TLS mismatch indicators. Boolean detection verdicts for bot, VPN, tor, proxy, incognito, tampering, headless, VM, devtools, privacy browser, high activity, IP blocklist, and velocity. `result` (boolean) and `probability` (0.0–1.0). `result` (boolean) and `confidence` (0.0–1.0). `result` (boolean). True if traffic originates from a known Tor exit node. `result` (boolean). True if a residential or public proxy is detected. `result` (boolean). True if the browser is in private/incognito mode. `result` (boolean) and `anomalyScore` (0–100). Indicates API or signal tampering. `result` (boolean). True if headless browser or automation markers are detected. `result` (boolean). True if the device appears to be running inside a VM. `result` (boolean). True if browser DevTools were open during signal collection. `result` (boolean) and `name` (string | null). Identifies known privacy-hardened browsers such as Brave or Tor Browser. `result` (boolean). True if this visitor has unusually high event velocity. `result` (boolean). True if the IP appears in a known blocklist. Rolling window counts of `distinctIp`, `distinctCountry`, and total `events` for the past `5m`, `1h`, and `24h`. Full signal data — only available via Server API, never returned client-side. All 41 client-side signal results. Keys map to signal names (e.g., `canvas`, `webgl`, `audio`). See [Signal Types Reference](/sdk/signals-reference) for the full schema. Server-side signals extracted at the edge including `asn`, `geo`, `tls`, `vpnDetection`, `ipNetwork`, `http`, and `consistency`. Customer-provided identifier, if set. Customer-provided metadata, if set. Whether this event was manually flagged as suspect. Unix timestamp (milliseconds) when this event occurred. ISO 8601 timestamp when the event was stored. ## Error Responses | Status | Meaning | |--------|---------| | `401` | Missing or invalid secret API key | | `404` | Event not found or belongs to a different customer | --- # POST /v1/identify > Identify a device and get a stable visitor ID. Source: https://docs.fingerprintiq.com/api/identify ## Endpoint ``` POST https://fingerprintiq.com/v1/identify ``` The JavaScript SDK calls this endpoint automatically. You only need to call it directly if you're building a custom SDK or integrating without the official package. ## Request ### Headers Must be `application/json`. Your API key. See [Authentication](/api/authentication) for details. ### Body Object containing all collected client-side signals. Each signal is a `SignalResult` with a `value` and `duration` (collection time in ms). See [Signal Types Reference](/sdk/signals-reference) for the full schema. Unix timestamp in milliseconds when signal collection began on the client. The page URL where identification occurred. Used for analytics and domain allowlist verification. The referring page URL, if available. Arbitrary key-value metadata to attach to this identification event. Stored with the event and returned via the Server API. Useful for associating a fingerprint with session context (e.g., `{ "page": "checkout", "experiment": "v2" }`). A string identifier to associate with this event — typically an authenticated user ID, session ID, or order ID. Enables querying all events linked to a specific user via the Server API. ```bash cURL curl -X POST https://fingerprintiq.com/v1/identify \ -H "Content-Type: application/json" \ -H "X-API-Key: fiq_live_your_api_key" \ -d '{ "signals": { "canvas": { "value": { "hash": "a3f2b1c4...", "isFarbled": false }, "duration": 12.5 }, "webgl": { "value": { "renderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Max)", "vendor": "Google Inc. (Apple)" }, "duration": 3.2 }, "audio": { "value": { "hash": "d4e5f6...", "sampleRate": 44100 }, "duration": 45.0 }, "navigator": { "value": { "hardwareConcurrency": 8, "platform": "MacIntel", "languages": ["en-US"] }, "duration": 0.1 } }, "timestamp": 1712000000000, "url": "https://yoursite.com/checkout", "referrer": "https://yoursite.com/" }' ``` ```javascript SDK (recommended) const fiq = new FingerprintIQ({ apiKey: 'fiq_live_your_api_key' }); const result = await fiq.identify({ tag: { page: 'checkout' }, linkedId: 'user_123', }); // The SDK builds and sends the full payload automatically ``` ## Response ```json 200 OK { "requestId": "req_01hns3k6tez83695a6t7", "visitorId": "iq_01hns3k6tez83695a6t714s6n1", "confidence": 1.0, "botProbability": 0.05, "suspectScore": 0, "visitCount": 3, "firstSeenAt": 1712000000000, "lastSeenAt": 1712200000000, "riskFactors": [], "verdicts": { "bot": { "result": false, "probability": 0.05 }, "vpn": { "result": false, "confidence": 0.92 }, "tor": { "result": false }, "proxy": { "result": false }, "incognito": { "result": false }, "tampering": { "result": false, "anomalyScore": 0 }, "headless": { "result": false }, "virtualMachine": { "result": false }, "devtools": { "result": false }, "privacyBrowser": { "result": false, "name": null }, "highActivity": { "result": false }, "ipBlocklist": { "result": false } }, "ip": "203.0.113.42", "ipLocation": { "country": "US", "city": "New York", "region": "NY", "latitude": 40.71, "longitude": -74.01 }, "timestamp": 1712000003000 } ``` ```json 401 Unauthorized { "error": "Invalid or missing API key" } ``` ```json 429 Rate Limited { "error": "Monthly API call limit exceeded", "usage": 10001, "limit": 10000 } ``` ## Response Fields The identify endpoint returns a **compact response** — verdicts and top-level scores only. Raw signal data (all 41 client signals plus full server signals) is available server-side via `GET /v1/events/:requestId`. See the [Events API](/api/events) reference. Unique identifier for this specific identification event, in the format `req_`. Pass this to your backend and use it to retrieve full signal data via the Server API. Stable device identifier in the format `iq_`. This is the primary output — store it to link future visits to the same device. Signal confidence score from 0.0 to 1.0. Low values indicate that many signals failed to collect, which may itself indicate a bot or privacy tool. Scores below 0.7 warrant extra scrutiny. Bot likelihood score from 0.0 to 1.0. Combines datacenter ASN, headless markers, software renderer, API tampering, and UA/TLS mismatch indicators. Composite risk score from 0 to 100. Higher values indicate more risk signals. Combines bot probability, VPN/proxy detection, incognito mode, tampering, and high-activity flags into a single actionable number. Total number of times this device has been identified. A value of 1 means first-time visitor. Unix timestamp (milliseconds) of the first identification for this device. Unix timestamp (milliseconds) of the most recent identification for this device prior to this event. Per-signal boolean verdicts for this visit. `result` (boolean) plus `probability` (0.0–1.0). `result` (boolean) plus `confidence` (0.0–1.0). `result` (boolean). `result` (boolean). `result` (boolean). `result` (boolean) plus `anomalyScore` (0–100). `result` (boolean). `result` (boolean). `result` (boolean). `result` (boolean) plus `name` (string or null — e.g., `"Brave"`, `"Tor Browser"`). `result` (boolean). True when the device has been seen an unusually high number of times in a short window. `result` (boolean). True when the IP is on a known blocklist. The client's IP address as seen by the edge worker. Geolocation derived from the IP address: `country` (ISO 3166-1 alpha-2), `city`, `region`, `latitude`, `longitude`. Active risk indicators for this visit. Possible values include: - `HEADLESS_BROWSER` — WebDriver or automation markers detected - `SOFTWARE_RENDERER` — SwiftShader or LLVMpipe GPU - `TOR_EXIT_NODE` — Traffic from Tor network - `DATACENTER_ASN` — Traffic from cloud provider - `UA_TLS_MISMATCH` — User-Agent inconsistent with TLS fingerprint - `LOW_RTT_COHERENCE` — Geo coherence below 0.3 (likely VPN) Unix timestamp (milliseconds) of this specific identification event. ## Error Responses | Status | Meaning | |--------|---------| | `400` | Invalid JSON body or missing required `signals` field | | `401` | Invalid or missing `X-API-Key` header | | `429` | Rate limit or monthly quota exceeded | | `500` | Internal server error — retry with exponential backoff | --- # PUT /v1/events/:requestId > Update event metadata including linkedId, tag, and suspect flag. Source: https://docs.fingerprintiq.com/api/update-event ## Endpoint ``` PUT https://fingerprintiq.com/v1/events/:requestId ``` This is a **Server API** endpoint. It requires a secret API key (`fiq_secret_*`) and must only be called from your backend, never from client-side code. ## Authentication Include your secret key in the Authorization header: ``` Authorization: Bearer fiq_secret_your_key_here ``` ## Path Parameters The `requestId` of the event to update. ## Request Body Associate this event with a customer-provided identifier, such as your internal user ID. Useful for linking events to authenticated users after the fact. Arbitrary JSON metadata to attach to the event. Must be a flat key-value object. Replaces any existing tag. Manually flag this event as suspect (`true`) or clear an existing suspect flag (`false`). Flagged events are included in fraud review queues and analytics. ```bash Flag event as suspect curl -X PUT https://fingerprintiq.com/v1/events/req_01hns3k6tez83695a6t7 \ -H "Authorization: Bearer fiq_secret_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "suspect": true, "tag": { "reason": "manual_review", "reviewer": "fraud-team" } }' ``` ```bash Set linkedId after authentication curl -X PUT https://fingerprintiq.com/v1/events/req_01hns3k6tez83695a6t7 \ -H "Authorization: Bearer fiq_secret_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "linkedId": "user_456", "tag": { "plan": "enterprise", "region": "us-east" } }' ``` ```bash Clear suspect flag curl -X PUT https://fingerprintiq.com/v1/events/req_01hns3k6tez83695a6t7 \ -H "Authorization: Bearer fiq_secret_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "suspect": false }' ``` ## Response ```json 200 OK { "updated": true } ``` ```json 400 Bad Request { "error": "Invalid request body: 'tag' must be a flat key-value object" } ``` ```json 404 Not Found { "error": "Event not found" } ``` ## Response Fields Always `true` when the update succeeds. Use `suspect: true` as part of a chargeback or fraud response workflow. Once flagged, the event appears in your Suspect Events dashboard and can trigger downstream webhooks. You can retrieve all suspect events via [GET /v1/events/search](/api/events-search) with `suspect=true`. ## Error Responses | Status | Meaning | |--------|---------| | `400` | Invalid JSON body or unsupported field value | | `401` | Missing or invalid secret API key | | `404` | Event not found or belongs to a different customer | --- # GET /v1/demo/visits/:visitorId > Retrieve visit history for a visitor. Source: https://docs.fingerprintiq.com/api/visits ## Endpoint ``` GET https://fingerprintiq.com/v1/demo/visits/:visitorId ``` This endpoint is server-side only. Always call it from your backend using a secret API key — never from the browser. ## Request ### Path Parameters The visitor ID returned by the `/v1/identify` endpoint. Format: `iq_` (e.g., `iq_01hns3k6tez83695a6t714s6n1`). ### Headers Your server-side secret API key. ```bash curl https://fingerprintiq.com/v1/demo/visits/iq_01hns3k6tez83695a6t714s6n1 \ -H "X-API-Key: fiq_live_your_secret_key" ``` ## Response ```json { "visits": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "timestamp": 1712000003000, "country": "US", "city": "New York City", "region": "New York", "asn": 7922, "asnOrg": "Comcast Cable Communications, Inc", "asnCategory": "RESIDENTIAL_ISP", "botProbability": 0.05, "confidence": 1.0, "riskFactors": [], "signals": { "client": { "..." }, "server": { "..." } } } ], "totalVisits": 3, "firstSeenAt": 1712000000000 } ``` ## Response Fields The 20 most recent visits for this visitor, ordered newest first. Unique UUID for this specific visit record. Unix timestamp (milliseconds) when this visit occurred. ISO 3166-1 alpha-2 country code (e.g., `US`, `GB`, `DE`). City name based on IP geolocation. Region or state name. Autonomous System Number of the visitor's IP address. Organization name for the ASN (e.g., "Comcast Cable Communications, Inc"). ASN category: `RESIDENTIAL_ISP`, `MOBILE_CARRIER`, `DATACENTER`, `VPN_COMMERCIAL`, `TOR_EXIT`, `EDUCATION`, `GOVERNMENT`, or `CDN`. Bot likelihood score (0.0–1.0) for this visit. Signal confidence score (0.0–1.0) for this visit. Risk indicators active during this visit. Full signal snapshot for this visit, including all client and server signals. Total number of visits ever recorded for this visitor (not just the 20 returned in `visits`). Unix timestamp (milliseconds) of the very first visit from this device. Use `totalVisits` and `firstSeenAt` to assess trust. A visitor seen 50 times over 3 months is very different from one seen 3 times in the last 10 minutes. --- # Webhooks > Receive real-time notifications for fingerprint events. Source: https://docs.fingerprintiq.com/api/webhooks Webhooks are available on Builder plans and above. See [Pricing](/introduction#pricing) to upgrade. ## Overview Configure webhooks to receive HTTP POST notifications when fingerprint events occur in real time — without polling the API. ### Event Types | Event | Description | |-------|-------------| | `visit.new` | A device is identified for the first time | | `visit.returning` | A previously seen device visits again | | `visit.flagged` | A visit triggered one or more risk factors | | `threat.detected` | Bot probability exceeded 0.7 on a visit | ## Webhook Payload All events share the same payload structure. The `event` field identifies the type. ```json { "event": "visit.returning", "timestamp": 1712000003000, "data": { "visitorId": "iq_01hns3k6tez83695a6t714s6n1", "visitCount": 5, "botProbability": 0.05, "country": "US", "riskFactors": [] } } ``` ### Payload Fields The event type. One of `visit.new`, `visit.returning`, `visit.flagged`, or `threat.detected`. Unix timestamp (milliseconds) when the event occurred on the FingerprintIQ edge. Event data specific to the fingerprint event. The stable device identifier that triggered this event. Total visit count for this device at the time of the event. Bot probability score (0.0–1.0) for this visit. ISO country code of the visitor's IP at the time of the event. Risk indicators active during the visit that triggered this event. Will be non-empty for `visit.flagged` and `threat.detected` events. ## Signature Verification All webhook deliveries include an `X-FIQ-Signature` header containing an HMAC-SHA256 signature computed over the raw request body. Always verify this signature before processing the event. ```javascript Node.js function verifyWebhook(rawBody, signature, webhookSecret) { const expected = crypto .createHmac('sha256', webhookSecret) .update(rawBody) .digest('hex'); // Use timingSafeEqual to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex') ); } // Express example app.post('/webhooks/fingerprintiq', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-fiq-signature']; if (!verifyWebhook(req.body, sig, process.env.FIQ_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); // handle event res.status(200).send('OK'); }); ``` ```typescript Hono (Cloudflare Workers) const app = new Hono(); app.post('/webhooks/fingerprintiq', async (c) => { const rawBody = await c.req.text(); const sig = c.req.header('x-fiq-signature') ?? ''; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(c.env.FIQ_WEBHOOK_SECRET), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const expected = await crypto.subtle.sign( 'HMAC', key, new TextEncoder().encode(rawBody) ); const expectedHex = Array.from(new Uint8Array(expected)) .map(b => b.toString(16).padStart(2, '0')) .join(''); if (sig !== expectedHex) { return c.text('Invalid signature', 401); } const event = JSON.parse(rawBody); // handle event return c.text('OK'); }); ``` Always use `crypto.timingSafeEqual` (Node.js) or `crypto.subtle` (Web Crypto) for signature comparison. A simple string comparison (`===`) is vulnerable to timing attacks that can leak your webhook secret. ## Delivery and Retries FingerprintIQ delivers webhooks with the following guarantees: - Deliveries time out after **5 seconds** — your endpoint must respond within this window - Events are retried up to **3 times** with exponential backoff (1s, 5s, 25s) on non-200 responses or timeouts - Respond with `2xx` as quickly as possible and process the event asynchronously (e.g., push to a queue) Return a `200 OK` immediately and process webhook events in a background job or queue. This prevents timeouts if your processing logic involves database writes or external API calls. --- # Changelog > API and SDK version history. Source: https://docs.fingerprintiq.com/changelog ## 2026-04-13 — CLI & AI agent device-code auth - **New endpoints `POST /api/cli/device-code` and `POST /api/cli/token`** — OAuth-style device authorization grant for CLIs and AI coding agents. Your tool requests a short verification URL, the user signs in and authorizes in the browser, and your tool polls for a `fiq_live_` key. The plaintext key is returned exactly once. - **New consent page `/cli/auth?code=…`** — shows the requesting client name, the verification code, and a project picker. The issued key appears in Dashboard → API Keys and can be revoked there. - **Skill & docs updated** — the FingerprintIQ AI agent skill and [`/api/authentication`](/api/authentication) now describe both paths: paste an existing key, or run the device-code flow. ## 2026-04-13 — SDK framework support: React, Vue, Next.js, Express, Python ### New packages and subpaths - **`@fingerprintiq/js@0.3.0`** — three new subpath exports: - `@fingerprintiq/js/react` — `useFingerprintIQ` hook with auto-identify on mount - `@fingerprintiq/js/vue` — Vue 3 composable with the same shape - `@fingerprintiq/js/next` — `"use client"` re-export of the React hook for App Router - **`@fingerprintiq/server@0.3.0`** — new `@fingerprintiq/server/express` subpath with an Express middleware that mirrors the existing Hono one. - **`fingerprintiq==0.1.0`** — new Python SDK on PyPI, covering all three products: - **Identify** — server-side visitor lookup via `FingerprintIQ.lookup(visitor_id=...)`, sync + async - **Sentinel** — `SentinelMiddleware` (FastAPI/Starlette) + bare-ASGI variant - **Pulse** — `Pulse` class with machine fingerprinting and background-thread event flushing, byte-compatible with the Node SDK fingerprint hash ### Notes - React, Vue, and `react-dom` are declared as optional peer dependencies on `@fingerprintiq/js` — install only the ones you use. - The Python Pulse fingerprint matches the Node Pulse fingerprint for the same machine, so users who run both SDKs on the same host show up as a single entity in the dashboard. - The dashboard "Get Started" wizard now shows real code snippets for every supported tile, including the new Python and Express options. ## 2026-04-11 — API v1.1: Compact Response + Server API ### Breaking Changes - **Identify response restructured**: The `POST /v1/identify` endpoint now returns a compact response. Raw `signals.client` and `signals.server` are no longer included. Use the new Server API to access full signal data. - **New `requestId` field**: Every identify response now includes a `requestId` for fetching full details. ### New Features - **Sealed client results**: AES-256-GCM encrypted payload containing full event data, delivered client-side for server-side decryption. Eliminates the need for a separate Server API call. - **Server API**: Five new endpoints for server-side event management - `GET /v1/events/:requestId` — full event details with all signals - `GET /v1/events` — search and filter events - `PUT /v1/events/:requestId` — update event metadata (tag, linkedId, suspect) - `GET /v1/visitors/:visitorId` — paginated visit history - `DELETE /v1/visitors/:visitorId` — GDPR data deletion - **Secret API keys**: New `fiq_secret_*` key type for server-side API access - **Verdicts**: 12 boolean detection results (bot, vpn, tor, proxy, incognito, tampering, headless, virtualMachine, devtools, privacyBrowser, highActivity, ipBlocklist) plus velocity signals - **Suspect score**: Composite risk score (0-100) combining all signals - **Tag & linkedId**: Attach metadata to identification events - **SDK caching**: Optional sessionStorage/localStorage caching to reduce API calls - **Velocity signals**: Time-windowed counts (5m/1h/24h) for distinct IPs, countries, and events per visitor - **IP location**: Top-level `ip` and `ipLocation` fields in response - **lastSeenAt**: Timestamp of the visitor's previous identification ### SDK Changes - `identify()` now accepts `{ tag, linkedId, timeout }` options - New `cache` constructor option - Updated `IdentifyResponse` type with all new fields - New exported types: `Verdicts`, `IpLocation`, `SybilRisk`, `IdentifyOptions`, `CacheConfig` ## 2026-04-10 — Initial Public Beta - 41 client-side signals with parallel collection - Server-side TLS (JA4), ASN, geo, VPN detection - Web3 wallet detection and Sybil risk scoring - Dashboard with analytics, rules engine, webhooks - Sentinel and Pulse products - Free tier: 25,000 identifications/month --- # Understanding Confidence Scores > How confidence, botProbability, and suspectScore work — and what thresholds to use in your application. Source: https://docs.fingerprintiq.com/concepts/confidence-score Every FingerprintIQ identification returns three scores. Each measures something different, and they are designed to be used in combination — not in isolation. --- ## confidence (0.0 – 1.0) `confidence` measures **how completely the SDK was able to collect signals**. It is a signal collection success rate, not a measure of whether the `visitorId` is accurate. A `confidence` of 1.0 means all configured signals were collected and passed consistency checks. A `confidence` of 0.6 means roughly 40% of signals failed to collect — due to browser restrictions, privacy extensions, or active suppression. **What lowers confidence:** - Privacy browsers (Brave, Firefox RFP, Tor Browser) that block or randomize APIs - Extensions like CanvasBlocker or JShelter that intercept and modify signal APIs - Very old or non-standard browsers that lack WebGL, AudioContext, or other APIs - Automated browsers that stub APIs with null returns **Confidence does not mean accuracy.** A bot that successfully returns all signals can have a high confidence score. Confidence is best used to identify sessions where signal collection was degraded — those sessions deserve extra scrutiny, not automatic trust. --- ## botProbability (0.0 – 1.0) `botProbability` measures the **likelihood that this session is automated**. It is computed from a combination of signals and is independent of confidence. Inputs to `botProbability`: - Datacenter or hosting ASN (AWS, GCP, Azure) - Software WebGL renderer (`SwiftShader`, `llvmpipe`) - `navigator.webdriver` presence - Chrome DevTools Protocol runtime objects on `window` - TLS JA4 fingerprint mismatch with declared User-Agent - Canvas/audio API returning constant or zeroed values - Signal collection timing anomalies - HTTP header ordering inconsistent with the declared browser ### Threshold recommendations | Range | Interpretation | Recommended response | |-------|---------------|---------------------| | 0.0 – 0.1 | Very likely human | Allow | | 0.1 – 0.3 | Low risk, minor signals | Log and monitor | | 0.3 – 0.5 | Suspicious | CAPTCHA or friction | | 0.5 – 0.7 | Likely automated | Step-up challenge | | 0.7 – 1.0 | Very likely bot | Block | --- ## suspectScore (0 – 100) `suspectScore` is a **composite risk integer** that aggregates all risk signals — including bot probability, verdicts, velocity, infrastructure type, and signal quality — into a single number. It is designed to be the primary decision threshold for most applications. Unlike `botProbability`, which focuses on automation, `suspectScore` incorporates the full risk picture: VPN usage, proxy detection, IP blocklist hits, tampering, headless markers, and velocity spikes. ### Threshold recommendations | Range | Meaning | Recommended action | |-------|---------|-------------------| | 0–20 | Low risk | Allow | | 20–50 | Moderate risk | Log, apply soft rate limits | | 50–70 | Elevated risk | CAPTCHA, step-up auth, or manual review | | 70–100 | High risk | Block or hard challenge | --- ## How privacy browsers affect scores Privacy-hardened browsers are specifically designed to interfere with fingerprinting APIs. FingerprintIQ detects these and adjusts scores accordingly. ### Brave Browser Brave randomizes canvas, WebGL, audio, and font enumeration results via "farbling" — controlled, reproducible noise. FingerprintIQ detects farbled outputs and lowers `confidence`. Brave users also trigger `verdicts.privacyBrowser.result = true` with `name: "Brave"`. **Expected scores for a Brave user:** - `confidence`: 0.6–0.75 (farbled signals reduce collection quality) - `botProbability`: typically low unless other automation signals are present - `verdicts.privacyBrowser`: `{ result: true, name: "Brave" }` ### Firefox RFP (Resist Fingerprinting) Firefox with `privacy.resistFingerprinting = true` (enabled by default in Tor Browser) returns spoofed values for many APIs: a 100×100 canvas, a uniform audio context, a fixed screen size. This significantly degrades signal quality. **Expected scores:** - `confidence`: 0.4–0.6 - `verdicts.privacyBrowser.result`: `true` - `riskFactors`: may include `CANVAS_FARBLED`, `AUDIO_STUBBED` ### Tor Browser Tor Browser uses Firefox RFP plus routes traffic through Tor exit nodes. **Expected scores:** - `confidence`: 0.4–0.6 - `verdicts.tor.result`: `true` - `botProbability`: elevated due to Tor infrastructure signals ### CanvasBlocker and similar extensions Extensions that block or randomize specific APIs are detectable through inconsistencies — for example, a canvas that returns a valid hash but the hash is known to be extension-generated, or an AudioContext that returns a constant rather than hardware-specific value. **Expected scores:** - `confidence`: 0.7–0.85 - `riskFactors`: may include `CANVAS_FARBLED` --- ## Decision tree for common scenarios ``` Is verdicts.headless.result === true? YES → Block (automated browser) NO ↓ Is verdicts.bot.probability > 0.8? YES → Block NO ↓ Is suspectScore > 70? YES → Block or hard challenge NO ↓ Is suspectScore > 50? YES → CAPTCHA / step-up auth NO ↓ Is confidence < 0.6? YES → Apply additional scrutiny (privacy browser or extension) Check verdicts.privacyBrowser — if true, this may be a legitimate user NO ↓ Is suspectScore > 20? YES → Log for monitoring, apply soft rate limits NO ↓ → Allow ``` --- ## Using scores together The three scores are complementary: ```typescript function assessRisk(result: IdentifyResponse): 'block' | 'challenge' | 'monitor' | 'allow' { const { confidence, botProbability, suspectScore, verdicts } = result; // Hard blocks if (verdicts.headless.result) return 'block'; if (botProbability > 0.8) return 'block'; if (suspectScore > 70) return 'block'; // Challenge zone if (suspectScore > 50) return 'challenge'; if (botProbability > 0.5) return 'challenge'; // Low confidence — privacy browser, not necessarily malicious if (confidence < 0.6) { // Check if it's a known privacy browser (often legitimate users) if (verdicts.privacyBrowser?.result) return 'monitor'; // Unknown low-confidence session — more suspicious return 'challenge'; } // Low-level monitoring if (suspectScore > 20 || botProbability > 0.1) return 'monitor'; return 'allow'; } ``` For most applications, `suspectScore` is the right primary signal — it integrates everything. Use `botProbability` and `confidence` as tiebreakers or for specific use cases (e.g., `confidence` is useful for detecting privacy extensions on forms where you need high-fidelity identification). --- # Smart Signals Overview > How verdicts, velocity signals, and raw signals relate — and when to use each. Source: https://docs.fingerprintiq.com/concepts/smart-signals FingerprintIQ separates its output into two tiers: **raw signals** (the evidence collected from the browser and network) and **verdicts** (the conclusions computed from that evidence). Understanding this distinction helps you build faster, more accurate risk logic. --- ## What verdicts are A verdict is a **computed boolean detection result** — a yes/no answer to a specific question about the current session. Verdicts are derived from multiple raw signals and server-side analysis. They are designed to be actionable without requiring you to understand the underlying signal mechanics. **Raw signal:** The WebGL renderer string is `"ANGLE (SwiftShader Device (Subzero), Google Inc.)"` **Verdict conclusion:** `verdicts.bot.result = true` (software renderer is a bot indicator) Verdicts abstract away the complexity of interpreting dozens of signals. Instead of building and maintaining logic that maps signal values to risk conclusions, you consume the verdict and act on it. --- ## All 12 verdicts Automated browser or non-browser HTTP client. Combines ASN, renderer, webdriver markers, and TLS fingerprint analysis. Automation framework detected — Selenium, Playwright, Puppeteer, or similar. Detects `navigator.webdriver`, CDP runtime objects, and API stubs. Commercial VPN service detected. IP belongs to a known VPN provider ASN. Includes a `confidence` score (0.0–1.0). Traffic originates from a Tor exit node. Matched against the Tor Project's published exit node list. Residential or public proxy detected. Harder to detect than datacenter VPNs; uses RTT coherence and IP reuse signals. SDK or browser API tampering detected. Indicates an active attempt to spoof or suppress fingerprint signals. Includes an `anomalyScore` (0–100). Device appears to be running inside a virtual machine — based on GPU vendor strings, screen resolution patterns, and CPUID markers. Browser is in private/incognito mode. Detected via storage API behavior and browser-specific private mode markers. Browser DevTools were open during signal collection. Useful for detecting manual tampering attempts. A privacy-hardened browser was detected — Brave, Tor Browser, or Firefox with RFP enabled. Includes a `name` field. This visitor has unusually high event velocity relative to typical users. Derived from the velocity signal windows. The visitor's IP appears in a known abuse or threat intelligence blocklist. --- ## Velocity signals explained Velocity signals track how frequently a `visitorId` has been seen and from how many distinct IPs and countries — across three time windows. ```typescript interface VelocityVerdict { distinctIp: { '5m': number; // distinct IPs used by this visitorId in the last 5 minutes '1h': number; // last 1 hour '24h': number; // last 24 hours }; distinctCountry: { '5m': number; '1h': number; '24h': number; }; events: { '5m': number; // total identification events from this visitorId '1h': number; '24h': number; }; } ``` **What counts as an event:** Every call to the `/v1/identify` endpoint increments the `events` counter for that `visitorId`. **What counts as a distinct IP:** The IP address seen at the edge. If a VPN user rotates through 5 exit nodes, `distinctIp['1h']` will be 5. **Interpreting velocity:** | Pattern | What it indicates | |---------|------------------| | `events['5m'] > 10` | Scripted automation or aggressive polling | | `distinctIp['1h'] > 5` | VPN rotation or bot farm IP cycling | | `distinctCountry['24h'] > 3` | Impossible travel (or multi-region bot farm) | | `events['24h'] > 100` | High-activity bot or very active legitimate user | The `verdicts.highActivity` flag is set automatically when velocity crosses FingerprintIQ's internal thresholds. Use the raw velocity numbers when you want to apply custom thresholds. --- ## Verdicts vs raw signals | | Verdicts | Raw Signals | |---|----------|------------| | What they are | Computed conclusions | Evidence collected from browser/network | | Available in | Client-side `identify()` response | Server API (`GET /v1/events/:requestId`) only | | Format | Boolean + optional score | Varies by signal type | | Use case | Fast client and server-side decisions | Deep forensic analysis, model training, custom scoring | | Example | `verdicts.bot.result = true` | `signals.client.webgl.renderer = "SwiftShader..."` | **Verdicts are conclusions. Signals are evidence.** A verdict like `verdicts.vpn.result = true` is derived from signals including: ASN category, IP range membership in VPN provider CIDRs, RTT coherence, and HTTP header patterns. You don't need to evaluate each of these yourself — the verdict does it for you. --- ## When to use verdicts Use verdicts for the majority of application logic: ```typescript // At checkout — simple verdict check const result = await fiq.identify(); if (result.verdicts.bot.result) { blockRequest('bot_detected'); return; } if (result.verdicts.headless.result) { blockRequest('automation_detected'); return; } if (result.verdicts.tor.result || result.verdicts.proxy.result) { requireStepUp('high_risk_network'); return; } ``` Verdicts are computed at the edge, returned in the identify response, and can be used client-side for non-critical decisions or server-side (after verification via Events API) for security-critical decisions. --- ## When to use raw signals Use raw signals when you need to: - Build a **custom scoring model** trained on your own fraud data - **Investigate a specific event** — understand exactly why a session was flagged - **Audit or replay** signal data for retroactive analysis - Create **allowlists** based on known device characteristics (e.g., your own monitoring infrastructure) - Combine FingerprintIQ signals with your own behavioral or transactional data Raw signals are only available via the Server API. They are never returned to the client: ```typescript // Backend: fetch raw signals for custom analysis const event = await fetch( `https://fingerprintiq.com/v1/events/${requestId}`, { headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}` } } ).then(r => r.json()); // Raw signal examples const { webgl, canvas, audio, fonts } = event.signals.client; const { asn, geo, tls } = event.signals.server; // Custom rule: allow known internal monitoring tools if ( webgl.renderer.includes('ANGLE (Apple') && asn.org === 'Your Company ASN' && tls.ja4 === KNOWN_INTERNAL_JA4 ) { return allowRequest(); } ``` Start with verdicts. They cover the vast majority of fraud detection use cases with minimal implementation complexity. Graduate to raw signals when you have specific requirements — custom models, forensic workflows, or operational allowlists — that verdicts alone cannot satisfy. --- # Detecting Account Fraud > Use device fingerprinting to detect multi-accounting, account takeover, and identity recycling. Source: https://docs.fingerprintiq.com/guides/account-fraud Account fraud comes in two main forms: **multi-accounting** (one person operating many accounts to abuse a system) and **account takeover** (a bad actor seizing control of someone else's account). FingerprintIQ's stable `visitorId`, historical visit data, and verdicts give you reliable signals for both. --- ## Multi-accounting detection Multi-accounting happens when a single device registers or uses multiple accounts to exploit trial limits, referral bonuses, review systems, or marketplace policies. Because `visitorId` is stable across cookie clearing and incognito mode, a device that creates 10 accounts will always produce the same `visitorId`. ### Strategy 1. On account creation and login, call `identify()` and pass the resulting `requestId` to your backend. 2. On your backend, fetch the full event from the Events API to get the verified `visitorId`. 3. Look up how many accounts in your database are linked to that `visitorId`. 4. If the count exceeds your threshold, block or flag the action. ```typescript // Backend handler (Hono / Node.js) const app = new Hono(); app.post('/api/register', async (c) => { const { requestId, email, password } = await c.req.json(); // Step 1: Verify the fingerprint event server-side const event = await fetchFingerprintEvent(requestId); if (!event) { return c.json({ error: 'Fingerprint verification failed' }, 400); } const { visitorId, verdicts } = event; // Step 2: Count existing accounts for this device const existingAccounts = await db .select({ count: count() }) .from(accounts) .where(eq(accounts.visitorId, visitorId)); const accountCount = existingAccounts[0].count; // Step 3: Enforce your multi-account policy if (accountCount >= 3) { await logFraudEvent({ type: 'multi_account_attempt', visitorId, existingCount: accountCount, ip: event.ip, }); return c.json({ error: 'Account limit reached for this device' }, 403); } // Step 4: Apply additional verdict signals if (verdicts.bot.result || verdicts.tampering.result) { return c.json({ error: 'Registration blocked' }, 403); } // Proceed with account creation, store visitorId const account = await createAccount({ email, password, visitorId }); return c.json({ accountId: account.id }); }); async function fetchFingerprintEvent(requestId: string) { const res = await fetch( `https://fingerprintiq.com/v1/events/${requestId}`, { headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}`, }, } ); if (!res.ok) return null; return res.json(); } ``` Store `visitorId` alongside your user record at signup time. This makes subsequent lookups a simple indexed query rather than a live API call. --- ## Account takeover detection Account takeover (ATO) happens when an attacker gains access to credentials (via phishing, credential stuffing, or data breach) and logs in from a new device. The fingerprint of the attacker's device will differ from the `visitorId` the legitimate user has always logged in from. Key signals for ATO detection: - **New `visitorId`** on a known account — the device has never authenticated this account before - **`firstSeenAt` / `visitCount`** — a brand-new device with no history is riskier than a device seen 100 times over 6 months - **Velocity** — multiple failed logins from different `visitorId` values in a short window - **Verdicts** — `vpn`, `proxy`, `tor`, or `datacenter` ASN on a consumer account login ### Strategy ```typescript app.post('/api/login', async (c) => { const { requestId, email, password } = await c.req.json(); // Verify credentials first const user = await verifyCredentials(email, password); if (!user) { return c.json({ error: 'Invalid credentials' }, 401); } // Fetch fingerprint event const event = await fetchFingerprintEvent(requestId); if (!event) { // Degrade gracefully — don't block legitimate users on API failure return c.json({ token: issueToken(user) }); } const { visitorId, verdicts, firstSeenAt, visitCount } = event; const riskSignals: string[] = []; // Check 1: Is this a device we've seen log into this account before? const knownDevice = await db.query.devices.findFirst({ where: and( eq(devices.userId, user.id), eq(devices.visitorId, visitorId) ), }); if (!knownDevice) { riskSignals.push('new_device'); // New device + very fresh fingerprint = high ATO risk const deviceAgeMs = Date.now() - firstSeenAt; const deviceAgeHours = deviceAgeMs / 1000 / 60 / 60; if (deviceAgeHours < 1 && visitCount < 3) { riskSignals.push('brand_new_device'); } } // Check 2: Infrastructure risk — datacenter, VPN, Tor if (verdicts.tor.result) riskSignals.push('tor_exit_node'); if (verdicts.vpn.result) riskSignals.push('vpn_detected'); if (verdicts.proxy.result) riskSignals.push('proxy_detected'); // Check 3: Automation — headless / bot if (verdicts.headless.result) riskSignals.push('headless_browser'); if (verdicts.bot.result) riskSignals.push('bot_detected'); // Decision const riskScore = computeRiskScore(riskSignals); if (riskScore >= 80) { // Block outright — bot or Tor login on a new device return c.json({ error: 'Login blocked due to suspicious activity' }, 403); } if (riskScore >= 40) { // Require step-up authentication (email OTP, TOTP) const challengeToken = await issueMfaChallenge(user.id); return c.json({ requiresMfa: true, challengeToken, reason: riskSignals, }, 200); } // Low risk — issue token and record the device await db.insert(devices).values({ userId: user.id, visitorId }).onConflictDoNothing(); return c.json({ token: issueToken(user) }); }); function computeRiskScore(signals: string[]): number { const weights: Record = { 'brand_new_device': 40, 'new_device': 20, 'tor_exit_node': 50, 'vpn_detected': 20, 'proxy_detected': 30, 'headless_browser': 60, 'bot_detected': 70, }; return signals.reduce((sum, s) => sum + (weights[s] ?? 0), 0); } ``` --- ## Using verdicts as additional signals Beyond `visitorId`, the verdict set provides boolean detection results you can use as additional fraud signals: | Verdict | Relevance to account fraud | |---------|---------------------------| | `verdicts.bot` | Credential stuffing, automated registrations | | `verdicts.headless` | Selenium/Playwright-based account creation farms | | `verdicts.tampering` | SDK bypass attempts — signals are being spoofed | | `verdicts.vpn` | Risk signal for logins on consumer accounts | | `verdicts.tor` | High-risk; legitimate users rarely log in over Tor | | `verdicts.virtualMachine` | Mass registration from cloud VMs | | `verdicts.velocity` | Burst of events from the same device — scripted behavior | Verdicts are available in the client-side `identify()` response for lightweight checks. For the full verdict set including velocity windows and raw signals, always verify server-side via `GET /v1/events/:requestId`. --- ## Recommended thresholds | Scenario | Action | |----------|--------| | Same `visitorId`, 3+ accounts | Require manual review | | Same `visitorId`, 5+ accounts | Hard block | | New device + `vpn` + `visitCount < 3` | Step-up MFA | | New device + `tor` or `headless` | Block login | | `verdicts.tampering.result === true` | Block and log — active evasion attempt | | `suspectScore > 70` on registration | Queue for review | --- # AI Agent Skill > Drop a single SKILL.md into Claude Code, Codex, Cursor, Windsurf, Aider, or any agent that supports AGENTS.md — and let it integrate FingerprintIQ without hallucinating. Source: https://docs.fingerprintiq.com/guides/ai-skill # FingerprintIQ Agent Skill Modern AI coding agents can install project-specific or global "skills" — small Markdown files that teach them how to use a tool correctly. FingerprintIQ ships one canonical `SKILL.md` that works with all of them. Once installed, you can say things like *"add fingerprintiq bot detection to the checkout page"* or *"block AI agents from `/api/public` with sentinel"* and the agent will write idiomatic code using real package names, real API shapes, and real framework subpaths — not whatever it remembered from training data. The skill covers all three FingerprintIQ products — **Identify** (browser fingerprinting), **Sentinel** (server caller classification), and **Pulse** (CLI analytics) — in a single file. Install it once. ## What's in the skill - Product decision guide (Identify vs Sentinel vs Pulse) - Install + code snippets for JS, TypeScript, React, Next.js, Vue, Hono, Express, FastAPI, Starlette, and Python - Full `IdentifyResponse` and `SentinelResult` type definitions - API endpoint reference for custom integrations - Server-side verification pattern (don't trust the client alone) - Common mistakes checklist (hardcoded keys, missing allowlist, wallet-popup misuse, etc.) Canonical URL: ``` https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md ``` Browse on GitHub: [`fingerprintiq/skills`](https://github.com/fingerprintiq/skills) --- ## Install per tool ### Claude Code Claude Code loads skills from `~/.claude/skills//SKILL.md` (user-wide) or `.claude/skills//SKILL.md` (project-local). The file already has the correct YAML frontmatter. **User-wide (recommended):** ```bash mkdir -p ~/.claude/skills/fingerprintiq curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ -o ~/.claude/skills/fingerprintiq/SKILL.md ``` **Project-local:** ```bash mkdir -p .claude/skills/fingerprintiq curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ -o .claude/skills/fingerprintiq/SKILL.md ``` Restart Claude Code (or run `/skills` to refresh). The skill will auto-trigger whenever you mention fingerprintiq, visitor IDs, bot detection, Sentinel, Pulse, or any `@fingerprintiq/*` package. You can also invoke it explicitly with `/skill fingerprintiq`. ### Codex / OpenAI Codex CLI Codex CLI reads `AGENTS.md` files up the directory tree. Append the skill to your project's `AGENTS.md`, or link to it: ```bash curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ >> AGENTS.md ``` Or, if you want the skill available across all projects, drop it in `~/.codex/AGENTS.md`: ```bash mkdir -p ~/.codex curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ >> ~/.codex/AGENTS.md ``` The YAML frontmatter is harmless — Codex reads the body as instructions. ### Cursor Cursor supports two places for rule files: project-level `.cursor/rules/*.mdc` and legacy `.cursorrules`. **Project rules (preferred, Cursor 0.42+):** ```bash mkdir -p .cursor/rules curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ -o .cursor/rules/fingerprintiq.mdc ``` **Legacy `.cursorrules`:** ```bash curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ >> .cursorrules ``` ### Windsurf Windsurf reads `.windsurfrules` at the project root and global rules from the Settings pane. ```bash curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ >> .windsurfrules ``` ### Aider Aider picks up `CONVENTIONS.md` automatically when started with `--read CONVENTIONS.md`, or you can load any file inline: ```bash curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ -o .aider/fingerprintiq.md aider --read .aider/fingerprintiq.md ``` ### Continue.dev Add the skill as a custom context provider or a rule in `~/.continue/config.json`: ```json { "rules": [ "https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md" ] } ``` Or inline it in a project `.continue/rules.md`. ### Zed Zed's assistant reads `.zed/assistant_rules.md` at the project root: ```bash mkdir -p .zed curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ > .zed/assistant_rules.md ``` ### Generic — any agent that reads `AGENTS.md` The `AGENTS.md` convention is supported by most modern coding agents, including Sourcegraph Cody, Replit Ghostwriter, and several open-source agents. Drop the skill content into `AGENTS.md` at your project root: ```bash curl -fsSL https://raw.githubusercontent.com/fingerprintiq/skills/main/fingerprintiq/SKILL.md \ >> AGENTS.md ``` --- ## Using it Once installed, try prompts like: - *"Add fingerprintiq to this Next.js app so I can block bot checkouts."* - *"Wire up @fingerprintiq/server sentinel middleware on my Hono routes and log the caller type."* - *"Add Pulse to my CLI so I can see unique machines and command frequency. Python."* - *"Look up a visitor by id from our FastAPI backend using fingerprintiq."* The agent should now reach for the right product, the right package subpath (e.g. `@fingerprintiq/js/next`), the right framework middleware (e.g. `@fingerprintiq/server/hono`), and — crucially — verify trust decisions on the server rather than trusting the client response. The skill reminds the agent to never trust the client `identify()` response alone for access decisions. Expect it to add a server-side `GET /v1/events/:requestId` verification step automatically. --- ## Keeping it up to date The skill lives in the public [fingerprintiq/skills repo](https://github.com/fingerprintiq/skills) and is automatically mirrored from our private SDK monorepo on every release, so the `main` branch always matches the latest published SDK surface. Re-run the `curl` command any time you want the latest version — it's idempotent and safe to overwrite. If you want to pin to a specific commit: ``` https://raw.githubusercontent.com/fingerprintiq/skills//fingerprintiq/SKILL.md ``` --- ## Why a skill beats "just read the docs" Coding agents don't reliably read full documentation sites mid-task — they pattern-match to what they already know. That means unless you hand them the exact current shape of `@fingerprintiq/js`, `@fingerprintiq/server`, and `@fingerprintiq/pulse`, they tend to: - Import from `@fingerprint/js` or `fingerprintjs-pro` (wrong package) - Pass `options.apiKey` to a static method instead of `new FingerprintIQ({ apiKey })` - Make trust decisions on the client response alone - Call `requestWalletConnection()` on page load, which triggers a MetaMask popup - Confuse Sentinel (server-side classification) with Identify (client-side fingerprint) The skill is a ~400-line cheat sheet that lives in the agent's context when it's relevant and stays out of the way when it isn't. One `curl`, and every future FingerprintIQ integration in that workspace starts from a correct baseline. ## Questions or contributions File an issue on [fingerprintiq/skills](https://github.com/fingerprintiq/skills/issues). If the skill gets something wrong — a renamed export, a new framework subpath, a missing pattern — open an issue and we'll cut a new version. --- # Bot Detection > Detect and block automated browsers, headless tools, and bot traffic using FingerprintIQ verdicts. Source: https://docs.fingerprintiq.com/guides/bot-detection FingerprintIQ detects bots through a layered approach: behavioral markers left by automation frameworks, hardware and rendering inconsistencies that headless browsers can't easily fake, and server-side signals like TLS fingerprint mismatches that reveal non-browser HTTP clients. --- ## verdicts.bot `verdicts.bot` is the primary bot signal. It combines multiple indicators into a single probability score. ```typescript interface BotVerdict { result: boolean; // true if bot is likely probability: number; // 0.0–1.0 bot likelihood } ``` Inputs that contribute to `botProbability`: - Datacenter or hosting ASN (AWS, Google Cloud, Azure, DigitalOcean) - Software WebGL renderer (`SwiftShader`, `llvmpipe`, `Mesa`) - Missing or spoofed browser APIs (`Notification`, `Permissions`, `Navigator`) - TLS fingerprint mismatch — JA4 hash doesn't match the declared `User-Agent` - Timing anomalies — signal collection completed impossibly fast or slow - Canvas/audio API returning zeroed or constant values (farbling or stubbing) - HTTP header ordering inconsistent with the declared browser ### Recommended thresholds | `botProbability` | Interpretation | Recommended action | |-----------------|----------------|-------------------| | 0.0 – 0.1 | Very likely human | Allow | | 0.1 – 0.3 | Slightly elevated | Log, monitor | | 0.3 – 0.5 | Suspicious | CAPTCHA challenge | | 0.5 – 0.7 | Likely bot | Require CAPTCHA or step-up | | 0.7 – 1.0 | Very likely bot | Block | --- ## verdicts.headless `verdicts.headless` specifically targets automation frameworks: Selenium, Playwright, Puppeteer, and similar tools. These tools modify or leave traces in the browser environment that differ from a genuine user session. ```typescript interface HeadlessVerdict { result: boolean; // true if automation markers detected } ``` Detection techniques include: - `navigator.webdriver` property present and `true` - CDP (Chrome DevTools Protocol) runtime objects exposed on `window` - Missing or incorrect speech synthesis voice list - Inconsistent `window.chrome` or extension API presence - Permissions API returning unexpected values - Plugin array inconsistencies Sophisticated bots use stealth patches (e.g., `puppeteer-extra-plugin-stealth`) to hide headless markers. FingerprintIQ detects many stealth patches themselves as anomalies. However, treat `verdicts.headless` as a strong signal, not an absolute guarantee. --- ## riskFactors `riskFactors` is a string array of specific risk indicators that contributed to the bot or suspect determination. Useful for logging and debugging. Common values: | Factor | Meaning | |--------|---------| | `DATACENTER_IP` | IP belongs to a known cloud/hosting ASN | | `SOFTWARE_RENDERER` | GPU is a software renderer (no real GPU) | | `WEBDRIVER_PRESENT` | `navigator.webdriver` is `true` | | `CDP_RUNTIME` | Chrome DevTools Protocol objects exposed | | `TLS_UA_MISMATCH` | TLS fingerprint doesn't match declared browser | | `CANVAS_FARBLED` | Canvas API output is randomized (anti-fingerprint extension) | | `AUDIO_STUBBED` | AudioContext returns constant/zero values | | `MISSING_BROWSER_APIS` | Expected browser APIs are absent or stubbed | | `IP_BLOCKLIST` | IP is on a known abuse blocklist | | `VELOCITY_SPIKE` | Unusually high event rate for this device | ```typescript const result = await fiq.identify(); if (result.riskFactors.includes('WEBDRIVER_PRESENT')) { // Selenium or Playwright detected blockRequest(); } if (result.riskFactors.includes('DATACENTER_IP') && result.riskFactors.includes('SOFTWARE_RENDERER')) { // Cloud VM with no GPU — classic bot farm indicator blockRequest(); } ``` --- ## suspectScore `suspectScore` is a composite integer from 0 to 100 that aggregates all risk signals — bot probability, verdict flags, velocity, and infrastructure signals — into a single number. | Score range | Meaning | Recommended action | |-------------|---------|-------------------| | 0–20 | Low risk | Allow | | 20–50 | Moderate risk | Monitor | | 50–70 | Elevated risk | CAPTCHA / rate-limit | | 70–100 | High risk | Block | --- ## Blocking bots at the edge The most effective bot blocking happens at the edge, before requests hit your origin. Here is a pattern using a Cloudflare Worker: ```typescript // workers/bot-guard.ts export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // Only gate protected paths const protectedPaths = ['/api/register', '/api/checkout', '/api/submit']; if (!protectedPaths.some(p => url.pathname.startsWith(p))) { return fetch(request); } const body = await request.clone().json().catch(() => null); const requestId = body?.requestId; if (!requestId) { return new Response(JSON.stringify({ error: 'Missing security token' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } // Verify the fingerprint event server-side const eventRes = await fetch( `https://fingerprintiq.com/v1/events/${requestId}`, { headers: { Authorization: `Bearer ${env.FINGERPRINT_SECRET_KEY}` }, } ); if (!eventRes.ok) { // API unavailable — pass through with a warning header const response = await fetch(request); return new Response(response.body, { ...response, headers: { ...Object.fromEntries(response.headers), 'X-Fingerprint-Status': 'unavailable', }, }); } const event = await eventRes.json(); const { verdicts, botProbability, suspectScore } = event; // Immediate block conditions if ( verdicts.headless.result || verdicts.bot.probability > 0.8 || verdicts.tampering.result || suspectScore > 80 ) { return new Response(JSON.stringify({ error: 'Request blocked' }), { status: 403, headers: { 'Content-Type': 'application/json' }, }); } // Add risk signals as request headers for origin logging const modifiedRequest = new Request(request, { headers: { ...Object.fromEntries(request.headers), 'X-FIQ-Visitor-Id': event.visitorId, 'X-FIQ-Bot-Probability': botProbability.toString(), 'X-FIQ-Suspect-Score': suspectScore.toString(), 'X-FIQ-Risk-Factors': event.riskFactors.join(','), }, }); return fetch(modifiedRequest); }, }; ``` --- ## Graceful degradation Bot detection should never break the experience for legitimate users. If `identify()` fails (network error, timeout), do not block the request — log the failure and allow it through. ```javascript async function getFingerprint() { try { return await fiq.identify(); } catch (err) { console.warn('FingerprintIQ unavailable:', err.message); // Return a sentinel object so downstream code doesn't need null checks return { requestId: null, botProbability: 0, suspectScore: 0, verdicts: { bot: { result: false, probability: 0 }, headless: { result: false } }, riskFactors: [], cacheHit: false, }; } } ``` On your backend, treat a missing or invalid `requestId` as a risk signal worth logging, but don't block on it alone. Legitimate users sometimes have the identify call fail due to network issues or ad blockers. Combine the missing fingerprint signal with other signals (failed login rate, IP reputation) before deciding to block. --- # Privacy & GDPR Compliance > How FingerprintIQ handles personal data, what it stores, and how to handle deletion requests. Source: https://docs.fingerprintiq.com/guides/gdpr FingerprintIQ is designed to operate without collecting personal data. This page explains what is stored, what is not, and how to implement the right-to-erasure obligation under GDPR Article 17. This documentation describes FingerprintIQ's technical architecture. It is not legal advice. Consult your legal counsel to determine your obligations under GDPR, CCPA, or other applicable law for your specific use case and jurisdiction. --- ## What FingerprintIQ stores FingerprintIQ stores the following per identification event: | Field | Description | Personal data? | |-------|-------------|---------------| | `visitorId` | Random ULID — no connection to real-world identity | No | | `requestId` | Random ULID for this specific event | No | | `timestamp` | Unix timestamp of the event | No | | `country` | Country code derived from IP (e.g., `US`) | No | | `city` | City name derived from IP | Borderline (not stored server-side by default) | | `botProbability` | Computed score, 0.0–1.0 | No | | `confidence` | Computed score, 0.0–1.0 | No | | `suspectScore` | Computed integer, 0–100 | No | | `verdicts` | Boolean detection flags | No | | `tag` | Customer-provided metadata | Depends on what you put here | | `linkedId` | Customer-provided identifier | Depends on what you put here | | `signals.client` | Hashed signal outputs (e.g., canvas hash) | No — hashed, not raw | | `signals.server` | ASN, TLS fingerprint, RTT | No | ### What is NOT stored - **IP address** — the full IP is used during signal collection but is not persisted after the event is processed. Only the country/city are stored. - **User-Agent string** — used for consistency checks, not stored. - **Email, name, phone, or any PII** — never collected, never stored. - **Cookies** — FingerprintIQ does not set any cookies. - **Local storage or IndexedDB** — the SDK does not write to browser storage (unless you enable the optional response cache, which stores only the hashed API key prefix and the identification result). - **Cross-site tracking data** — FingerprintIQ has no mechanism to correlate a user's `visitorId` across different customers' domains. --- ## No cookies, no cross-site tracking FingerprintIQ is a **cookieless** fingerprinting system. It does not: - Set first-party or third-party cookies - Use `document.cookie`, `localStorage`, or `sessionStorage` as a persistent identifier (the optional SDK cache, if enabled, stores only the identification result for the current session — not a tracking identifier) - Share `visitorId` values between different customers' accounts - Maintain a cross-domain identity graph This design means FingerprintIQ falls outside the scope of ePrivacy Directive cookie consent requirements in most EU jurisdictions, and outside the scope of third-party tracking rules under CCPA. Your legal counsel should verify this for your specific deployment. --- ## GDPR Article 6 — Legal basis Device fingerprinting for fraud prevention and security typically relies on one of these legal bases under GDPR Article 6: **Article 6(1)(f) — Legitimate interests**: Fraud prevention, bot detection, and account security are recognized legitimate interests of a controller. A legitimate interests assessment (LIA) should document why fingerprinting is necessary, proportionate, and not overridden by the data subject's interests. **Article 6(1)(b) — Contract performance**: If fingerprinting is necessary to provide the contracted service (e.g., preventing fraud on a payment platform), this basis may apply. **Article 6(1)(a) — Consent**: If you rely on consent, you must obtain it before calling `fiq.identify()`. Because FingerprintIQ stores no PII, the data protection impact under any of these bases is low. However, if you store the `visitorId` alongside PII in your own database, that linkage may bring the `visitorId` into scope as personal data under GDPR's broad definition. --- ## Data retention Events are retained for **365 days** by default. After 365 days, the event record and all associated signal data are permanently deleted. You can configure a shorter retention period per API key in the dashboard under **Settings > Data Retention**. --- ## Right to erasure — DELETE /v1/visitors/:visitorId Under GDPR Article 17, data subjects may request deletion of their data. FingerprintIQ provides a Server API endpoint to delete all events associated with a `visitorId`. ### Endpoint ``` DELETE https://fingerprintiq.com/v1/visitors/:visitorId ``` This permanently deletes: - All identification events for this `visitorId` - All stored signal data - All tag and linkedId associations It does **not** delete your application's own database records — you must handle that separately. ### Example: deletion request handler ```typescript // Backend route for handling right-to-erasure requests app.delete('/api/gdpr/erasure', async (c) => { const { userId } = c.get('authUser'); // your authenticated user // Step 1: Look up the visitorId(s) associated with this user // You should have stored visitorId at signup/login time const userDevices = await db .select({ visitorId: devices.visitorId }) .from(devices) .where(eq(devices.userId, userId)); const deletionResults = await Promise.allSettled( userDevices.map(({ visitorId }) => deleteFromFingerprintIQ(visitorId)) ); // Step 2: Delete from your own database await db.delete(devices).where(eq(devices.userId, userId)); await db.delete(users).where(eq(users.id, userId)); // Step 3: Log the erasure for your compliance audit trail const succeeded = deletionResults.filter(r => r.status === 'fulfilled').length; const failed = deletionResults.filter(r => r.status === 'rejected').length; await logErasureRequest({ userId, requestedAt: new Date().toISOString(), devicesDeleted: succeeded, devicesFailed: failed, }); return c.json({ message: 'Your data has been deleted.', devicesErased: succeeded, }); }); async function deleteFromFingerprintIQ(visitorId: string): Promise { const res = await fetch( `https://fingerprintiq.com/v1/visitors/${visitorId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}`, }, } ); if (!res.ok && res.status !== 404) { // 404 means already deleted or never existed — that's fine throw new Error(`FingerprintIQ deletion failed: ${res.status}`); } } ``` ### Response ```json 204 No Content ``` On success, the API returns `204 No Content`. On `404`, the `visitorId` does not exist (already deleted or never created) — treat this as success in your erasure flow. --- ## Deletion by linkedId If you have stored events with a `linkedId` (your user's internal ID), you can also delete all events for that linked ID: ```bash DELETE https://fingerprintiq.com/v1/visitors?linkedId=user_01hns3k6te ``` This deletes all events where `linkedId` matches, regardless of which `visitorId` they were collected under. Useful when a user accessed your service from multiple devices. ```typescript async function deleteByLinkedId(userId: string): Promise { const res = await fetch( `https://fingerprintiq.com/v1/visitors?linkedId=${encodeURIComponent(userId)}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}`, }, } ); if (!res.ok && res.status !== 404) { throw new Error(`Deletion failed: ${res.status}`); } } ``` --- ## Privacy notices If your privacy policy references device fingerprinting, the following language may be useful as a starting point (consult your legal counsel): > We use FingerprintIQ device fingerprinting technology for fraud prevention and security purposes. This technology analyzes device characteristics (such as browser configuration, hardware identifiers, and network signals) to generate a pseudonymous device identifier. No personal information such as your name, email address, or IP address is stored by this system. The legal basis for this processing is our legitimate interest in preventing fraud and maintaining the security of our platform (Article 6(1)(f) GDPR). --- ## Summary checklist - Does not set cookies - Does not store IP addresses - Does not collect names, emails, phone numbers, or other PII - Does not track users across different websites or customers - Does not share data between customers - Store `visitorId` alongside user records only if needed for your use case - Document your legal basis in a Legitimate Interests Assessment if using Article 6(1)(f) - Handle right-to-erasure requests by calling `DELETE /v1/visitors/:visitorId` and deleting from your own database - Update your privacy policy to disclose the use of device fingerprinting - Do not store PII in the `tag` field — use opaque identifiers in `linkedId` Default retention is 365 days. To reduce it, go to **Dashboard > Settings > Data Retention** and set a shorter period. Changes apply to new events; existing events are not retroactively deleted until they reach the new retention age. --- # JavaScript SDK > Integrate FingerprintIQ into any JavaScript application. Source: https://docs.fingerprintiq.com/guides/javascript ## Installation ```bash npm npm install @fingerprintiq/js ``` ```bash yarn yarn add @fingerprintiq/js ``` ```bash pnpm pnpm add @fingerprintiq/js ``` ```html CDN ``` ## Basic Usage ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_your_key', endpoint: 'https://fingerprintiq.com', // default timeout: 10000, // 10 seconds (default) detectWallets: true, // detect Web3 wallets (default: true) }); // Identify the current device const result = await fiq.identify(); ``` Instantiate `FingerprintIQ` once at module load time, not inside an event handler or on each page load. The instance is lightweight and reusable across multiple `identify()` calls. ## Configuration Options Your API key from the dashboard. Use `fiq_live_` for production and `fiq_test_` for development. Test keys don't count toward your monthly quota. API endpoint URL. Defaults to `https://fingerprintiq.com`. Override this if you're using a proxy or a self-hosted instance. Request timeout in milliseconds. Defaults to `10000` (10 seconds). Increase this on slow networks or decrease it if you need faster failure. Enable Web3 wallet detection. Defaults to `true`. Set to `false` to skip wallet enumeration and save ~50 ms if you don't need Web3 signals. ## Response Type ```typescript interface IdentifyResponse { visitorId: string; // Stable device identifier visitCount: number; // Total visits by this device firstSeenAt: number; // Unix timestamp (ms) of first visit confidence: number; // 0.0 - 1.0 signal confidence score botProbability: number; // 0.0 - 1.0 bot likelihood score signals: { client: Record; // All 29 client signals server: { asn: { asn: number; org: string; category: string }; geo: { country: string; city: string; rttCoherence: number }; tls: { cipher: string; version: string; protocol: string }; consistency: string[]; }; }; riskFactors: string[]; // Active risk indicators timestamp: number; // Unix timestamp (ms) of this visit } ``` ## Error Handling ```javascript try { const result = await fiq.identify(); // handle result } catch (error) { if (error.message.includes('429')) { // Rate limited — retry after a delay, or show a fallback console.warn('FingerprintIQ rate limit hit'); } else if (error.message.includes('401')) { // Invalid or missing API key — check your configuration console.error('Invalid FingerprintIQ API key'); } else { // Network timeout or connectivity issue console.error('FingerprintIQ request failed:', error.message); } } ``` If `identify()` fails, your application should degrade gracefully — don't block the user experience. Log the error and continue without the fingerprint when possible. ## Use Cases ```javascript const result = await fiq.identify(); if (result.visitCount > 1) { // This device has been seen before — check trial status const hasUsedTrial = await checkTrialStatus(result.visitorId); if (hasUsedTrial) { showPaywall(); } } ``` Always verify `visitorId` server-side before making trial decisions. A client-side check is easy to bypass. ```javascript const result = await fiq.identify(); if (result.botProbability > 0.7) { // Very likely a bot — block or hard challenge blockRequest(); } else if (result.botProbability > 0.3) { // Suspicious — show CAPTCHA showCaptcha(); } else if (result.botProbability > 0.1) { // Slightly elevated — increase monitoring logSuspiciousActivity(result); } ``` ```javascript const result = await fiq.identify(); const wallets = result.signals.client.wallets; if (wallets && wallets.count > 0) { console.log('Detected wallets:', wallets.detected); // ["MetaMask", "Phantom"] // Same device with multiple wallet addresses = potential Sybil if (result.visitCount > 5 && wallets.multipleWallets) { flagForReview(result.visitorId); } } ``` If you want to route FingerprintIQ traffic through your own domain (to avoid ad blockers or maintain a first-party relationship), set a custom endpoint: ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', endpoint: 'https://fp.yoursite.com', // proxy to fingerprintiq.com }); ``` Configure your proxy to forward requests to `https://fingerprintiq.com` and pass through the `X-API-Key` header. Cloudflare Workers make an excellent proxy layer for this pattern. --- # Next.js Integration > Integrate FingerprintIQ with Next.js App Router. Source: https://docs.fingerprintiq.com/guides/nextjs ## Installation ```bash npm npm install @fingerprintiq/js ``` ```bash yarn yarn add @fingerprintiq/js ``` ```bash pnpm pnpm add @fingerprintiq/js ``` FingerprintIQ uses browser APIs (canvas, WebGL, audio context) and **cannot run in Server Components**. Any component that uses the SDK must be marked with `'use client'`. ## Using the built-in hook For most apps, the built-in Next.js hook is the fastest path: ```tsx // components/visitor-gate.tsx 'use client'; export function VisitorGate() { const { result, loading, error } = useFingerprintIQ({ apiKey: process.env.NEXT_PUBLIC_FIQ_API_KEY!, }); if (loading) return

Identifying…

; if (error) return

Error: {error.message}

; return

Visitor: {result?.visitorId}

; } ``` The `/next` subpath ships with `"use client";` baked in, so you can import it from any file and only mark your own wrapper component as a client component. Under the hood it re-exports the same hook as `@fingerprintiq/js/react` — choose whichever import path you prefer. If you need a custom wrapper (e.g. to integrate with your auth flow or route changes), see the "Custom hook" pattern below. ## Client-Side Identification Create a reusable hook in a client component: ```tsx // components/Fingerprint.tsx 'use client'; export function useFingerprint() { const [visitorId, setVisitorId] = useState(null); const [botProbability, setBotProbability] = useState(null); const fiqRef = useRef(null); useEffect(() => { async function init() { if (!fiqRef.current) { fiqRef.current = new FingerprintIQ({ apiKey: process.env.NEXT_PUBLIC_FIQ_API_KEY!, }); } const result = await fiqRef.current.identify(); setVisitorId(result.visitorId); setBotProbability(result.botProbability); } init().catch(console.error); }, []); return { visitorId, botProbability }; } ``` ## Server-Side Verification After identifying on the client, send the `visitorId` to your API route for verification. This prevents spoofed visitor IDs from bypassing your trust logic. ```typescript // app/api/verify/route.ts export async function POST(req: Request) { const { visitorId } = await req.json(); const res = await fetch( `https://fingerprintiq.com/v1/demo/visits/${visitorId}`, { headers: { 'X-API-Key': process.env.FIQ_SECRET_KEY!, 'Content-Type': 'application/json', }, } ); const data = await res.json(); const isLegitimate = data.totalVisits >= 1 && data.visits[0]?.confidence >= 0.8 && data.visits[0]?.botProbability < 0.3; return NextResponse.json({ verified: isLegitimate, isReturning: data.totalVisits > 1, firstSeen: data.firstSeenAt, totalVisits: data.totalVisits, }); } ``` ## Environment Variables ```env # .env.local NEXT_PUBLIC_FIQ_API_KEY=fiq_live_your_public_key FIQ_SECRET_KEY=fiq_live_your_secret_key ``` Never expose your secret API key to the client. The `NEXT_PUBLIC_` prefix makes variables available in the browser — only use it for your public key. The secret key must stay server-side only. ## Full Integration Example Putting it all together — identify on the client, verify on the server, gate a feature: ```tsx // app/checkout/page.tsx 'use client'; export default function CheckoutPage() { const { visitorId, botProbability } = useFingerprint(); useEffect(() => { if (!visitorId) return; // Send to your server for verification fetch('/api/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ visitorId }), }) .then(res => res.json()) .then(data => { if (!data.verified) { // Redirect or show challenge window.location.href = '/verify'; } }); }, [visitorId]); if (botProbability !== null && botProbability > 0.7) { return
Access denied. Suspicious activity detected.
; } return ; } ``` Add FingerprintIQ identification to your root layout so the visitor ID is available by the time users reach protected pages — avoiding a waterfall of identify → verify → render. For server-rendered pages, you can add bot detection at the middleware level. However, this requires passing the visitor ID via a cookie set during client-side identification: ```typescript // middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { const visitorId = request.cookies.get('fiq_visitor_id')?.value; if (!visitorId) { // First visit — allow through, fingerprint will run client-side return NextResponse.next(); } // Optional: verify visitorId against FingerprintIQ API // Be mindful of latency — this adds an extra API call to every request return NextResponse.next(); } export const config = { matcher: ['/checkout/:path*', '/api/protected/:path*'], }; ``` --- # Payment Fraud Prevention > Add device fingerprinting to your checkout flow to detect card testing, stolen cards, and high-risk orders before they process. Source: https://docs.fingerprintiq.com/guides/payment-fraud Payment fraud costs merchants an average of $3.60 for every $1 of fraud loss when chargebacks, fees, and operational costs are factored in. Device fingerprinting stops many fraud vectors before a card is ever charged — before your payment processor even sees the request. --- ## The pattern: identify at checkout, verify on the server The core integration is two steps: 1. **Client**: Call `fiq.identify()` at checkout page load (or on form submit). Pass the `requestId` to your order processing endpoint. 2. **Server**: Before charging the card, fetch the full event from the Events API and make a risk decision. This server-side verification step is critical — it prevents an attacker from replaying a `requestId` from a clean session, or from submitting a forged payload. ```typescript // 1. Client-side (checkout page) const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...' }); document.getElementById('pay-btn').addEventListener('click', async () => { // Collect the fingerprint when the user clicks Pay const fp = await fiq.identify({ tag: { page: 'checkout', cartTotal: getCartTotal() }, linkedId: currentUser?.id, }); // Include requestId in the order payload await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requestId: fp.requestId, cardToken: getCardToken(), amount: getCartTotal(), }), }); }); ``` --- ## Server-side verification middleware ```typescript // middleware/fingerprintGuard.ts (Hono) interface FingerprintEvent { visitorId: string; confidence: number; botProbability: number; suspectScore: number; verdicts: { bot: { result: boolean; probability: number }; vpn: { result: boolean }; tor: { result: boolean }; proxy: { result: boolean }; tampering: { result: boolean; anomalyScore: number }; headless: { result: boolean }; ipBlocklist: { result: boolean }; velocity: { events: { '5m': number; '1h': number; '24h': number }; distinctIp: { '5m': number; '1h': number; '24h': number }; }; }; ip: string; tag: Record; } export const fingerprintGuard = createMiddleware(async (c, next) => { const body = await c.req.json(); const { requestId } = body; if (!requestId) { return c.json({ error: 'Missing fingerprint data' }, 400); } // Fetch event from FingerprintIQ const res = await fetch( `https://fingerprintiq.com/v1/events/${requestId}`, { headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}`, }, } ); if (!res.ok) { // API unavailable — fail open or closed depending on your risk appetite // For high-value orders, fail closed: if (body.amount > 500_00) { // $500+ return c.json({ error: 'Unable to verify request security' }, 503); } // For lower-value orders, continue with degraded signal await next(); return; } const event: FingerprintEvent = await res.json(); // Attach to context for downstream handlers c.set('fingerprintEvent', event); await next(); }); ``` --- ## Decision matrix Apply this decision matrix after fetching the fingerprint event: ```typescript // lib/paymentRisk.ts interface RiskDecision { action: 'block' | 'review' | 'allow'; reason: string; riskScore: number; } export function evaluatePaymentRisk( event: FingerprintEvent, orderAmount: number ): RiskDecision { const { verdicts, confidence, botProbability, suspectScore } = event; // Hard blocks — never process these if (verdicts.bot.result && verdicts.bot.probability > 0.8) { return { action: 'block', reason: 'automated_bot', riskScore: 100 }; } if (verdicts.headless.result) { return { action: 'block', reason: 'headless_browser', riskScore: 95 }; } if (verdicts.tampering.result && verdicts.tampering.anomalyScore > 50) { return { action: 'block', reason: 'signal_tampering', riskScore: 90 }; } if (verdicts.tor.result) { return { action: 'block', reason: 'tor_exit_node', riskScore: 85 }; } if (verdicts.ipBlocklist.result) { return { action: 'block', reason: 'ip_blocklist', riskScore: 85 }; } // Composite score check if (suspectScore > 70) { return { action: 'block', reason: 'high_suspect_score', riskScore: suspectScore }; } // Review queue — proceed to payment processor but flag for manual review let riskScore = suspectScore; if (verdicts.vpn.result) riskScore += 20; if (verdicts.proxy.result) riskScore += 25; if (confidence < 0.6) riskScore += 15; // low signal quality if (botProbability > 0.3) riskScore += 20; // High-value orders get a tighter threshold const reviewThreshold = orderAmount > 200_00 ? 30 : 50; // cents if (riskScore >= reviewThreshold) { return { action: 'review', reason: 'elevated_risk', riskScore }; } return { action: 'allow', reason: 'low_risk', riskScore }; } ``` ### Order processing with the decision matrix ```typescript app.post('/api/orders', fingerprintGuard, async (c) => { const { cardToken, amount } = await c.req.json(); const event = c.get('fingerprintEvent'); const decision = evaluatePaymentRisk(event, amount); if (decision.action === 'block') { await logFraudBlock({ visitorId: event.visitorId, reason: decision.reason, amount, ip: event.ip, }); return c.json({ error: 'Payment declined' }, 402); } // Charge the card const charge = await stripe.paymentIntents.create({ amount, currency: 'usd', payment_method: cardToken, confirm: true, metadata: { visitorId: event.visitorId, riskScore: decision.riskScore.toString(), fingerprintRequestId: event.requestId, requiresReview: String(decision.action === 'review'), }, }); // Flag for manual review if needed if (decision.action === 'review') { await queueForReview({ orderId: charge.id, visitorId: event.visitorId, riskScore: decision.riskScore, reason: decision.reason, }); } return c.json({ orderId: charge.id, requiresReview: decision.action === 'review' }); }); ``` --- ## Velocity checks for card testing Card testing attacks submit small test charges (often $0.01–$1.00) against stolen card lists to validate which cards are active before making larger purchases. These attacks hit hard and fast — dozens of transactions per minute from the same or rotating devices. FingerprintIQ's velocity signals track `events`, `distinctIp`, and `distinctCountry` counts over 5-minute, 1-hour, and 24-hour windows. Use them to detect burst patterns: ```typescript function detectCardTesting(event: FingerprintEvent, amount: number): boolean { const { velocity } = event.verdicts; // More than 5 payment attempts from this device in the last 5 minutes if (velocity.events['5m'] > 5) return true; // More than 3 different IPs for this visitorId in the last hour // (VPN rotation or bot farm behavior) if (velocity.distinctIp['1h'] > 3) return true; // Low-value order from a device with very high 24-hour event count if (amount < 200 && velocity.events['24h'] > 50) return true; return false; } ``` Card testing attacks often use legitimate residential IPs and rotate them. IP-based rate limiting alone is insufficient — `visitorId` stability across IPs is what makes device fingerprinting effective here. --- ## Decision matrix summary | Signal | Threshold | Action | |--------|-----------|--------| | `verdicts.bot.probability` | > 0.8 | Block | | `verdicts.headless.result` | true | Block | | `verdicts.tor.result` | true | Block | | `verdicts.ipBlocklist.result` | true | Block | | `suspectScore` | > 70 | Block | | `verdicts.tampering.anomalyScore` | > 50 | Block | | `suspectScore` | 30–70 | Review | | `verdicts.vpn.result` | true | +20 to risk score | | `confidence` | < 0.6 | +15 to risk score | | `velocity.events['5m']` | > 5 | Card testing flag | | `velocity.distinctIp['1h']` | > 3 | IP rotation flag | --- # Pulse — CLI usage analytics Source: https://docs.fingerprintiq.com/guides/pulse # Pulse Usage analytics for CLI tools and AI agents. Tracks commands, machine fingerprints, and version adoption without collecting PII. ## Install ```bash npm install @fingerprintiq/pulse ``` ## Quick start ```typescript const pulse = new Pulse({ apiKey: 'fiq_live_...', tool: 'my-cli', version: '1.0.0', }); await pulse.track('deploy', { target: 'production', durationMs: 3400 }); await pulse.shutdown(); ``` ## Python Install the Python SDK: ```bash pip install fingerprintiq ``` Use it from any Python CLI: ```python from fingerprintiq.pulse import Pulse pulse = Pulse(api_key="fiq_live_...", tool="my-cli", version="1.2.3") pulse.track("deploy", metadata={"duration_ms": 1234, "success": True}) pulse.shutdown() # or let atexit handle it on process exit ``` Honors `DO_NOT_TRACK=1` and `FINGERPRINTIQ_OPTOUT=1` by default. Set `respect_opt_out=False` to override. Machine fingerprints are byte-compatible with the Node SDK — users who run both a Node and a Python CLI on the same machine show up as a single entity in Pulse. ## Privacy Pulse respects `DO_NOT_TRACK=1` and `FINGERPRINTIQ_OPTOUT=1`. All hardware identifiers (hostname, MAC addresses) are SHA-256 hashed before leaving the machine. The SDK never blocks your CLI or keeps the process alive. ## What you get - Unique machine counts (by hardware fingerprint, not IP) - Command frequency, error rates, durations - Environment breakdown (CI vs local vs container) - Version adoption curves - Machine retention (7d/30d return rates) ## Live demo Try it at [pulse-demo.fingerprintiq.com](https://pulse-demo.fingerprintiq.com) --- # React Integration > Use FingerprintIQ with React hooks. Source: https://docs.fingerprintiq.com/guides/react ## Installation ```bash npm npm install @fingerprintiq/js ``` ```bash yarn yarn add @fingerprintiq/js ``` ```bash pnpm pnpm add @fingerprintiq/js ``` `@fingerprintiq/js` ships a React hook at `@fingerprintiq/js/react`. Use it directly, or build your own wrapper with the patterns below. ## Using the built-in hook For most apps, the built-in hook is enough: ```tsx function Component() { const { result, loading, error, identify } = useFingerprintIQ({ apiKey: 'fiq_live_...', }); if (loading) return

Identifying…

; if (error) return

Error: {error.message}

; return

Visitor: {result?.visitorId} · Bot score: {result?.botProbability}

; } ``` The hook: - Instantiates the client once per component via `useRef` - Auto-calls `identify()` on mount (pass `manual: true` to opt out) - Exposes `loading`, `error`, and an `identify()` function for manual re-fetches - Is safe in React Strict Mode If you need custom lifecycle behavior, build your own wrapper using the pattern below. ## The `useFingerprintIQ` Hook Build a reusable hook that wraps the SDK. Using `useRef` ensures the `FingerprintIQ` instance is created only once per component tree, even in React Strict Mode. ```tsx interface FingerprintIQResult { visitorId: string; visitCount: number; confidence: number; botProbability: number; riskFactors: string[]; } function useFingerprintIQ(apiKey: string) { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fiqRef = useRef(null); if (!fiqRef.current) { fiqRef.current = new FingerprintIQ({ apiKey }); } const identify = async () => { setLoading(true); setError(null); try { const res = await fiqRef.current!.identify(); setResult(res); return res; } catch (err) { setError(err as Error); throw err; } finally { setLoading(false); } }; return { identify, result, loading, error }; } ``` ## Usage in Components ```tsx function LoginPage() { const { identify, result, loading, error } = useFingerprintIQ( process.env.NEXT_PUBLIC_FIQ_API_KEY! ); useEffect(() => { identify(); }, []); if (loading) return

Identifying device...

; if (error) return

Fingerprint unavailable

; return (

Visitor ID: {result?.visitorId}

Visit count: {result?.visitCount}

{result?.botProbability > 0.5 && (

⚠️ Suspicious activity detected

)}
); } ``` Call `identify()` as early as possible in the page lifecycle — ideally in a root layout or `useEffect` at the app level. This gives the SDK time to collect all signals before the user interacts with protected features. ## Context Provider Pattern For larger applications, expose fingerprint data via React Context to avoid prop drilling: ```tsx // contexts/FingerprintContext.tsx const FingerprintContext = createContext(null); export function FingerprintProvider({ children, apiKey }) { const [fingerprint, setFingerprint] = useState(null); const fiqRef = useRef(null); useEffect(() => { if (!fiqRef.current) { fiqRef.current = new FingerprintIQ({ apiKey }); } fiqRef.current .identify() .then(setFingerprint) .catch(console.error); }, [apiKey]); return ( {children} ); } export const useFingerprint = () => useContext(FingerprintContext); ``` ```tsx // app/layout.tsx or _app.tsx export default function RootLayout({ children }) { return ( {children} ); } ``` ## With Next.js App Router FingerprintIQ requires browser APIs and cannot run in Server Components. Mark any component using the SDK with `'use client'`. ```tsx // components/FingerprintProvider.tsx 'use client'; export function FingerprintProvider({ children }: { children: React.ReactNode }) { const fiqRef = useRef(null); useEffect(() => { if (!fiqRef.current) { fiqRef.current = new FingerprintIQ({ apiKey: process.env.NEXT_PUBLIC_FIQ_API_KEY!, }); } fiqRef.current.identify().catch(console.error); }, []); return <>{children}; } ``` Create a wrapper component that blocks rendering until a bot check passes: ```tsx 'use client'; import { useEffect, useState } from 'react'; import { useFingerprint } from '@/contexts/FingerprintContext'; export function BotGate({ children, threshold = 0.5, fallback = null }) { const fingerprint = useFingerprint(); if (!fingerprint) return
Verifying device...
; if (fingerprint.botProbability > threshold) return fallback; return children; } // Usage }> ```
--- # Sealed Client Results > Receive AES-256-GCM encrypted event data directly in the JS agent and decrypt it on your backend — no separate Server API call required. Source: https://docs.fingerprintiq.com/guides/sealed-results Sealed client results deliver the full event payload — the same data available from the Server API — directly inside the `identify()` response, encrypted so only your backend can read it. The browser never sees the raw signal data. Use sealed results when you need full event data synchronously on your backend without the latency of a separate Server API call. The browser receives an opaque encrypted blob; your server decrypts it and gets every signal and verdict immediately. --- ## How It Works The sealing flow involves a one-time key configuration and then operates transparently on every identification: ``` Dashboard Browser Your Backend │ │ │ │ 1. Generate key │ │ │◄────────────────────────│ │ │ │ │ │ 2. Activate key │ │ │◄────────────────────────│ │ │ │ │ │ │ 3. fiq.identify() │ │ │──────────────────────────────►│ │ │ │ │ │ 4. sealedResult (encrypted) │ │ │◄─────────────────────────────│ │ │ │ │ │ 5. POST sealedResult │ │ │──────────────────────────────►│ │ │ │ │ │ 6. unsealEventResponse() │ │ │ → full signals + verdicts │ ``` The JS agent calls `identify()` as normal. When a sealed key is active, the FingerprintIQ service encrypts the full event payload and returns it as `result.sealedResult`. Your frontend forwards that opaque string to your backend, which decrypts it using `@fingerprintiq/server`. --- ## Setup Go to **Dashboard → API Keys → Sealed Results** and click **Generate Key**. Copy the key — it is only shown once. ```bash npm install @fingerprintiq/server ``` Add the key to your environment and wire up a decryption endpoint. See the [Decryption example](#decryption-example) below. Set `FIQ_SEALED_KEY` in your environment (`.env`, Cloudflare Worker secrets, etc.): ```bash FIQ_SEALED_KEY=your_key_here ``` Back in **Dashboard → API Keys → Sealed Results**, click **Activate** next to the key you generated. After activation, all new `identify()` calls will include a `sealedResult` in the response. No frontend code changes are needed. Once the key is active, `result.sealedResult` is populated automatically: ```typescript const result = await fiq.identify(); // result.sealedResult is now a non-null encrypted string await fetch('/verify-fingerprint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sealedResult: result.sealedResult }), }); ``` --- ## Decryption Example ```typescript Node.js / Express app.post('/verify-fingerprint', async (req, res) => { const { sealedResult } = req.body; const event = unsealEventResponse(sealedResult, { keys: [process.env.FIQ_SEALED_KEY!], }); // Full event data available — same shape as the Server API response console.log(event.signals.client.canvas); console.log(event.signals.server.asn); console.log(event.verdicts.bot); const isLegitimate = event.verdicts.bot.probability < 0.3 && !event.verdicts.tampering.result && event.confidence >= 0.8; res.json({ verified: isLegitimate }); }); ``` ```typescript Hono const app = new Hono<{ Bindings: { FIQ_SEALED_KEY: string } }>(); app.post('/verify-fingerprint', async (c) => { const { sealedResult } = await c.req.json(); const event = unsealEventResponse(sealedResult, { keys: [c.env.FIQ_SEALED_KEY], }); const isLegitimate = event.verdicts.bot.probability < 0.3 && !event.verdicts.tampering.result && event.confidence >= 0.8; return c.json({ verified: isLegitimate }); }); ``` ```typescript Next.js API Route // app/api/verify-fingerprint/route.ts export async function POST(req: Request) { const { sealedResult } = await req.json(); const event = unsealEventResponse(sealedResult, { keys: [process.env.FIQ_SEALED_KEY!], }); return NextResponse.json({ verified: event.verdicts.bot.probability < 0.3 && !event.verdicts.tampering.result && event.confidence >= 0.8, visitorId: event.visitorId, }); } ``` --- ## Encryption Details The sealed payload uses **AES-256-GCM** with a unique random nonce per request, so no two sealed results are identical even for the same visitor. **Wire format** (base64-decoded bytes): | Bytes | Field | Value | |-------|-------|-------| | 0–3 | Magic header | `0x9E85DCED` | | 4–15 | Nonce | 12 random bytes | | 16–N-16 | Ciphertext | deflate-compressed JSON, AES-256-GCM encrypted | | N-16 to N | Auth tag | 16-byte GCM authentication tag | The GCM authentication tag makes the payload tamper-evident: decryption fails if any byte was modified in transit. --- ## Key Rotation Rotate sealed result keys at least every three months, or immediately after any suspected key compromise. The `keys` array in `unsealEventResponse` accepts multiple keys so you can rotate without downtime. Go to **Dashboard → API Keys → Sealed Results → Generate Key**. Copy the new key value. Update your environment and pass both keys in the `keys` array. The SDK tries each key in order until one succeeds: ```typescript const event = unsealEventResponse(sealedResult, { keys: [ process.env.FIQ_SEALED_KEY_NEW!, // try new key first process.env.FIQ_SEALED_KEY_OLD!, // fall back to old key ], }); ``` Click **Activate** next to the new key. New identifications will be encrypted with the new key. Old in-flight payloads are still decryptable via the fallback. After a few minutes (or once you confirm no decryption errors in your logs), remove the old key from the `keys` array and delete it from your environment. The recommended rotation interval is every 3 months. Add key rotation to your security runbook alongside certificate and secret rotation. --- ## Security Notes Never decrypt sealed results on the client. Doing so would require embedding the decryption key in browser code, exposing it to any user who opens DevTools. **Validate `requestId` uniqueness** — Each sealed payload contains a `requestId`. Store seen `requestId` values (e.g., in Redis with a short TTL) and reject duplicates. This prevents replay attacks where an attacker captures a sealed result from a legitimate session and re-submits it. ```typescript app.post('/verify-fingerprint', async (req, res) => { const { sealedResult } = req.body; const event = unsealEventResponse(sealedResult, { keys: [process.env.FIQ_SEALED_KEY!], }); // Replay protection: reject if this requestId was already processed const alreadySeen = await redis.get(`fiq:rid:${event.requestId}`); if (alreadySeen) { return res.status(400).json({ error: 'Duplicate request' }); } await redis.set(`fiq:rid:${event.requestId}`, '1', 'EX', 300); // 5-minute TTL // ... rest of your verification logic }); ``` **The sealed payload cannot be forged** — GCM authentication means any tampering with the ciphertext causes decryption to throw. You do not need to separately validate the payload structure; a successful `unsealEventResponse` call is proof the data came from FingerprintIQ. --- ## When to Use Sealed Results | Scenario | Recommendation | |----------|---------------| | You need full signal data on your backend synchronously | Use sealed results — no extra Server API round-trip | | You want signals invisible to the browser | Use sealed results — the browser only sees an opaque encrypted blob | | You only need `visitorId` and basic verdicts | Standard `identify()` response is sufficient | | You need to query historical events by `visitorId` | Use the [Server API](/guides/server-side) regardless | | You need to update event metadata (tag, linkedId) | Use the [Server API](/guides/server-side) `PUT /v1/events/:requestId` | Sealed results replace the need to call `GET /v1/events/:requestId` for the current identification. They do not replace the full Server API for historical queries, event updates, or GDPR deletion requests. --- # Sentinel — Server-side caller classification Source: https://docs.fingerprintiq.com/guides/sentinel # Sentinel Classify every API caller without requiring them to identify themselves. Sentinel inspects TLS fingerprints, header ordering, and request patterns to determine if a caller is a browser, AI agent, CLI tool, or bot. ## Install ```bash npm install @fingerprintiq/server ``` ## Hono middleware ```typescript const app = new Hono(); app.use('/api/*', sentinel({ apiKey: 'fiq_live_...', })); app.get('/api/data', (c) => { const caller = c.get('sentinel'); console.log(caller?.callerType); // "AI_AGENT", "CLI_TOOL", etc. return c.json({ ok: true }); }); ``` ## Express middleware ```typescript const app = express(); app.use(sentinel({ apiKey: 'fiq_live_...' })); app.get('/api/data', (req, res) => { const caller = req.sentinel; if (caller?.callerType === 'AI_AGENT') { return res.status(403).json({ error: 'AI agents blocked' }); } res.json({ ok: true }); }); ``` ## FastAPI middleware ```bash pip install 'fingerprintiq[fastapi]' ``` ```python from fastapi import FastAPI, Request from fingerprintiq.sentinel.fastapi import SentinelMiddleware app = FastAPI() app.add_middleware(SentinelMiddleware, api_key="fiq_live_...") @app.get("/api/data") def handler(request: Request): result = request.state.sentinel # SentinelResult | None if result and result.caller_type == "bot": return {"blocked": True} return {"ok": True} ``` For bare-ASGI (Starlette, Litestar, custom) use `fingerprintiq.sentinel.asgi.SentinelASGIMiddleware` with the same kwargs. ## Programmatic usage ```typescript const client = createSentinel({ apiKey: 'fiq_live_...' }); const result = await client.inspect(request); console.log(result.callerType); // "CLI_TOOL" console.log(result.callerConfidence); // 0.85 console.log(result.classification.library); // "curl" ``` ## Caller types | Type | What it means | |------|--------------| | BROWSER_HUMAN | Real browser, human timing | | BROWSER_AUTOMATED | Real browser, bot timing (Puppeteer, Playwright) | | AI_AGENT | Agent framework (LangChain, CrewAI, AutoGen) | | CLI_TOOL | Command-line tool (curl, httpie, wget) | | SDK_CLIENT | Server-side SDK (python-requests, node-fetch) | | BOT_SCRAPER | Web scraper or crawler | | UNKNOWN | Not enough signals to classify | ## Live demo Try it at [sentinel-demo.fingerprintiq.com](https://sentinel-demo.fingerprintiq.com) --- # Server-Side Verification > Validate fingerprints on your backend to prevent spoofing. Source: https://docs.fingerprintiq.com/guides/server-side ## Why Server-Side Verification? Client-side fingerprinting can be spoofed. A malicious user could intercept the SDK response and replay a different `visitorId` to your application. **Always verify on the server** before making trust decisions like granting trial access, processing payments, or allowing votes. This guide covers verifying `visitorId` values from the **Identify** product. If you want to classify API callers directly on the server without a client SDK at all, see the [Sentinel guide](/guides/sentinel) — it uses `@fingerprintiq/server` to inspect every incoming request at the middleware level. Never trust client-side data alone. The `visitorId` sent from the browser is user-controlled. Verify it against the FingerprintIQ API using your server-side secret key. ## The Verification Pattern Call `fiq.identify()` in the browser and get a `visitorId`. Include the `visitorId` in your API request body or as a header — not as a query parameter (avoid logging it in access logs). Fetch the visit history for that `visitorId` from the FingerprintIQ API using your server-side secret key. Check confidence, bot probability, recency, and any risk factors before making a decision. ## Client Code ```typescript // 1. Identify the device const result = await fiq.identify(); const { visitorId, confidence, botProbability } = result; // 2. Send visitorId to your server with the user's request const response = await fetch('/api/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ visitorId }), }); const { verified } = await response.json(); ``` ## Server Implementation ```typescript Node.js / Express app.post('/api/verify', async (req, res) => { const { visitorId } = req.body; const fiqRes = await fetch( `https://fingerprintiq.com/v1/demo/visits/${visitorId}`, { headers: { 'X-API-Key': process.env.FIQ_SECRET_KEY, 'Content-Type': 'application/json', }, } ); const data = await fiqRes.json(); const latestVisit = data.visits[0]; const isLegitimate = data.totalVisits >= 1 && latestVisit?.confidence >= 0.8 && latestVisit?.botProbability < 0.3 && Date.now() - latestVisit?.timestamp < 5 * 60 * 1000; // within 5 minutes res.json({ verified: isLegitimate, visits: data.totalVisits }); }); ``` ```typescript Hono const app = new Hono(); app.post('/api/verify', async (c) => { const { visitorId } = await c.req.json(); const fiqRes = await fetch( `https://fingerprintiq.com/v1/demo/visits/${visitorId}`, { headers: { 'X-API-Key': c.env.FIQ_SECRET_KEY }, } ); const data = await fiqRes.json(); const latestVisit = data.visits[0]; const isLegitimate = data.totalVisits >= 1 && latestVisit?.confidence >= 0.8 && latestVisit?.botProbability < 0.3; return c.json({ verified: isLegitimate }); }); ``` ```typescript Next.js API Route // app/api/verify/route.ts export async function POST(req: Request) { const { visitorId } = await req.json(); const fiqRes = await fetch( `https://fingerprintiq.com/v1/demo/visits/${visitorId}`, { headers: { 'X-API-Key': process.env.FIQ_SECRET_KEY! } } ); const data = await fiqRes.json(); const latestVisit = data.visits[0]; return NextResponse.json({ verified: latestVisit?.confidence >= 0.8 && latestVisit?.botProbability < 0.3, totalVisits: data.totalVisits, }); } ``` ## Security Best Practices The checks below are layered defenses. Implement all of them — skipping any one check leaves a gap an attacker can exploit. 1. **Verify recency** — Check that the latest visit timestamp is within the last 5 minutes. A `visitorId` from an hour ago should not grant access to a current session. 2. **Check confidence** — Reject fingerprints with `confidence` below 0.7. Low confidence means too many signals failed to collect, which is itself suspicious. 3. **Monitor bot probability** — Flag visitors with `botProbability` above 0.3 for review. Block those above 0.7. 4. **Check risk factors** — Inspect the `riskFactors` array for signals like `HEADLESS_BROWSER`, `TOR_EXIT_NODE`, `UA_TLS_MISMATCH`, or `SOFTWARE_RENDERER`. 5. **Use server-side API keys only** — The secret key used to call the FingerprintIQ API must never be exposed to the browser. Store verified `visitorId` values in your session after the first verification. This avoids a round-trip to the FingerprintIQ API on every authenticated request while still establishing the initial trust anchor. --- # VPN & Proxy Detection > Detect VPNs, Tor, residential proxies, and location spoofing using FingerprintIQ verdicts. Source: https://docs.fingerprintiq.com/guides/vpn-detection FingerprintIQ provides three distinct verdicts for anonymizing network services — `vpn`, `tor`, and `proxy` — plus a geo coherence signal (`rttCoherence`) for detecting location spoofing. Understanding the difference between these categories is important for tuning your risk policy. --- ## The three network verdicts ### verdicts.vpn A commercial VPN routes traffic through a datacenter IP on behalf of a subscriber. The user's real IP is hidden but replaced with a known, classifiable IP. ```typescript interface VpnVerdict { result: boolean; // true if commercial VPN detected confidence: number; // 0.0–1.0 detection confidence } ``` Detection signals: - ASN category is `VPN_COMMERCIAL` (ExpressVPN, NordVPN, Mullvad, etc.) - IP belongs to a known VPN provider IP range - IP is a datacenter IP but the TLS fingerprint matches a consumer browser - Geo mismatch between IP location and browser timezone/locale **Who uses VPNs:** - Privacy-conscious legitimate users (common in Germany, Netherlands) - Remote workers accessing geo-restricted content - Journalists, researchers, activists - Fraudsters hiding their real location ### verdicts.tor Tor routes traffic through a chain of volunteer relays, exiting through a known Tor exit node. The exit node IPs are publicly listed and can be matched deterministically. ```typescript interface TorVerdict { result: boolean; // true if Tor exit node detected — binary, no confidence score } ``` Detection: IP address matches the Tor Project's published exit node list, updated continuously. **Who uses Tor:** - Journalists and activists in high-surveillance countries - Privacy researchers - Fraudsters, dark web marketplace operators ### verdicts.proxy A proxy — specifically a residential or public proxy — routes traffic through an IP that appears to belong to a real home or mobile user. This is harder to detect than datacenter VPNs because the IP looks legitimate. ```typescript interface ProxyVerdict { result: boolean; // true if proxy detected } ``` Detection signals: - IP is on residential proxy network blocklists (e.g., Luminati/Bright Data, Oxylabs) - High reuse rate for the IP across many distinct devices - RTT to the edge is inconsistent with the claimed geo (see `rttCoherence`) - HTTP header anomalies (`X-Forwarded-For`, `Via` headers present) **Residential proxies are often used by:** - Sneaker bots and scalpers - Credential stuffing attacks - Ad fraud networks - Data scraping operations --- ## Geo coherence: detecting location spoofing `rttCoherence` is a value from 0.0 to 1.0 that measures how consistent the claimed location (from IP geolocation) is with the measured network latency from Cloudflare edge nodes. A user in New York claiming to be in New York will have low, consistent RTT to US edge nodes. A user in Moscow claiming to be in New York (via a VPN exit node) will have higher, inconsistent RTT that doesn't match expected New York latency. ```typescript interface GeoSignals { country: string; city: string; rttCoherence: number; // 1.0 = geo claim is plausible; 0.0 = implausible } // Available in the full event via Server API: const event = await fetchEvent(requestId); const { rttCoherence } = event.signals.server.geo; ``` | `rttCoherence` | Interpretation | |---------------|----------------| | 0.9 – 1.0 | Geo claim is highly plausible | | 0.7 – 0.9 | Minor inconsistency — normal variance | | 0.5 – 0.7 | Possible VPN or distant proxy | | 0.0 – 0.5 | Geo claim is implausible — likely spoofed | --- ## When to block vs warn vs allow VPN detection is nuanced. Blocking all VPN traffic will frustrate legitimate privacy-conscious users. The right policy depends on your threat model. | Scenario | Recommended action | |----------|-------------------| | `verdicts.tor.result === true` + consumer account | Block — Tor on consumer services is almost always evasion | | `verdicts.proxy.result === true` | Block or require CAPTCHA — residential proxies are rarely legitimate | | `verdicts.vpn.result === true` + `confidence > 0.9` + no prior account activity | Review | | `verdicts.vpn.result === true` + known returning user | Allow — likely a legitimate privacy user | | `verdicts.vpn.result === true` + high-risk action (password reset, payment) | Step-up authentication | | `rttCoherence < 0.4` + `verdicts.vpn.result === false` | Flag — possible novel VPN not yet classified | --- ## Enforcing geo restrictions If your service is geo-restricted (licensed content, regulatory compliance, sanctions), use the combination of IP geo and `rttCoherence` for a more reliable block than IP alone: ```typescript // backend/middleware/geoEnforcement.ts const ALLOWED_COUNTRIES = ['US', 'CA', 'GB', 'AU']; export const geoEnforcement = createMiddleware(async (c, next) => { const requestId = c.req.header('X-FIQ-Request-Id') ?? (await c.req.json().catch(() => ({}))).requestId; if (!requestId) { // No fingerprint — fall back to IP-only geo check const cfCountry = c.req.header('CF-IPCountry') ?? 'XX'; if (!ALLOWED_COUNTRIES.includes(cfCountry)) { return c.json({ error: 'Content not available in your region' }, 451); } return next(); } // Fetch full event for rich geo signals const event = await fetchFingerprintEvent(requestId); if (!event) { // API unavailable — fall back to Cloudflare's CF-IPCountry header const cfCountry = c.req.header('CF-IPCountry') ?? 'XX'; if (!ALLOWED_COUNTRIES.includes(cfCountry)) { return c.json({ error: 'Content not available in your region' }, 451); } return next(); } const { country } = event.signals.server.geo; const { rttCoherence } = event.signals.server.geo; const { vpn, tor, proxy } = event.verdicts; // Block disallowed countries if (!ALLOWED_COUNTRIES.includes(country)) { return c.json({ error: 'Content not available in your region' }, 451); } // Block geo spoofing: allowed country IP, but location is implausible if (rttCoherence < 0.4 && (vpn.result || proxy.result)) { return c.json( { error: 'Unable to verify your location. Disable VPN to continue.' }, 451 ); } // Allow VPN users in allowed regions (they pass country check above) // but log the VPN for compliance audit trail if (vpn.result || tor.result) { c.set('geoFlags', { vpn: vpn.result, tor: tor.result, rttCoherence }); } await next(); }); async function fetchFingerprintEvent(requestId: string) { const res = await fetch( `https://fingerprintiq.com/v1/events/${requestId}`, { headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}` }, } ); if (!res.ok) return null; return res.json(); } ``` --- ## Nuanced VPN policy: protecting returning users Blocking all VPNs will impact legitimate users. A better approach distinguishes between a new anonymous session and a known returning user who happens to use a VPN: ```typescript async function evaluateVpnRisk(event: FingerprintEvent, userId?: string): Promise<'allow' | 'warn' | 'block'> { const { vpn, tor, proxy } = event.verdicts; // Tor: always high-risk on consumer services if (tor.result) return 'block'; // Residential proxy: strong fraud signal if (proxy.result) return 'block'; // No VPN — nothing to evaluate if (!vpn.result) return 'allow'; // VPN detected — differentiate by user context if (userId) { // Known user on a VPN: check their history const userHistory = await getUserDeviceHistory(userId); const knownDevice = userHistory.some(d => d.visitorId === event.visitorId); if (knownDevice) { // Returning device + VPN: likely a privacy-conscious regular user return 'allow'; } // New device + VPN on a known account: could be ATO if (vpn.confidence > 0.8) return 'warn'; // step-up auth return 'allow'; } // Anonymous session + high-confidence VPN: warn if (vpn.confidence > 0.9) return 'warn'; return 'allow'; } ``` Combine `rttCoherence` with VPN verdicts for the strongest geo-enforcement signal. A VPN alone doesn't tell you the user is spoofing location — but a VPN combined with RTT that's inconsistent with the claimed country strongly suggests it. --- # How It Works > Understanding FingerprintIQ's multi-layered identification approach. Source: https://docs.fingerprintiq.com/how-it-works ## Architecture FingerprintIQ uses a **two-tier fingerprint** approach for maximum accuracy and stability. Rather than relying on a single hash that breaks when any signal changes, it separates signals into a stable core and a fuzzy supporting layer. ```mermaid flowchart TD A[Browser] -->|Collects 41 signals in parallel| B[FingerprintIQ SDK] B -->|Single POST request| C[Cloudflare Edge Worker] C -->|Extract server signals| D{Signal Processing} D --> E[TLS Fingerprint] D --> F[ASN Classification] D --> G[Geo Coherence] D --> H[Header Order] D --> I[UA Consistency] E & F & G & H & I --> J[Core Hash Computation] J --> K{Visitor Lookup} K -->|Match found ≥60% similarity| L[Returning Visitor\nIncrement visit count] K -->|No match| M[New Visitor\nCreate visitor record] L & M --> N[Return compact response\nrequestId + verdicts + scores] N --> A N --> O[(Events Store\nFull signals persisted)] O -->|GET /v1/events/:requestId| P[Your Backend Server] ``` The entire identification flow — from signal collection to response — completes in under 200 ms for most devices, including the Cloudflare edge round-trip. ## The Two-Tier Fingerprint ### Tier 1: Stable Core Hash Seven signals that almost never change for a given device form the primary lookup key. If a visitor's core hash matches an existing record, FingerprintIQ proceeds to similarity scoring. | Signal | Source | Stability | |--------|--------|-----------| | WebGL Renderer | GPU hardware model | Very High | | WebGL Vendor | GPU manufacturer | Very High | | Navigator | CPU cores, platform, languages | Very High | | Screen | Resolution + pixel ratio | High | | Math Hash | JS engine precision quirks | Very High | | Error Messages | JS engine error string format | Very High | | CSS Property Count | Browser engine version | High | | Platform Features | Available API surface | High | The core hash is computed entirely from hardware and engine characteristics — not from any mutable state like cookies, localStorage, or IP address. ### Tier 2: Supporting Signals (Similarity Score) Additional signals compute a **similarity score** (0.0 – 1.0) to handle edge cases where one or two core signals might shift due to a browser update or system change. | Signal | Type | Match Method | |--------|------|-------------| | Canvas Hash | Rendering | Exact | | Audio Hash | Rendering | Exact | | DOMRect Hash | Measurement | Exact | | Font Count | Enumeration | 10% tolerance | | WASM Timing | Performance | 10% tolerance | | Speech Hash | System voices | Exact | | Intl Hash | Locale formatting | Exact | | SVG Hash | SVG rendering | Exact | | Codec Hash | Media support | Exact | | Timezone | System clock | Exact | ### Fuzzy Matching Algorithm Hash the 7 stable signals into a deterministic lookup key. Query stored visitor records that share the same core hash. Compare supporting signals between the new visit and each candidate record. Compute a similarity percentage. If similarity is 60% or above, it's the same visitor — increment the visit count and return the existing `visitorId`. If below 60%, create a new visitor record. The 60% similarity threshold means up to 4 supporting signals can change simultaneously (e.g., after a major browser update) before FingerprintIQ creates a new visitor record. ## Compact Response + Server API FingerprintIQ uses a **two-tier data model**: the client gets a compact response for fast decision-making, while full signal data is available server-side for audit and advanced analysis. ### What `identify()` returns The identify endpoint returns a compact response with everything needed for real-time decisions: - `requestId` — unique event identifier for server-side lookup - `visitorId` — stable device identifier - `verdicts` — per-signal boolean results (bot, VPN, Tor, proxy, incognito, tampering, headless, VM, devtools, privacy browser, high activity, IP blocklist) - `suspectScore` — composite 0–100 risk score - `botProbability`, `confidence` — top-level scores - `ip` and `ipLocation` — network metadata Raw signals (all 41 client signals and full server-side signal data) are **not** included in the identify response. ### Accessing Full Signal Data Pass the `requestId` from the client to your backend, then call the Events API to retrieve the full event including all raw signals: ```typescript // Client side — pass requestId to your server const result = await fiq.identify(); // POST result.requestId to your backend // Server side — retrieve full event const res = await fetch( `https://fingerprintiq.com/v1/events/${requestId}`, { headers: { 'Authorization': 'Bearer fiq_secret_your_key' } } ); const event = await res.json(); // event.signals.client — all 41 client signal results // event.signals.server — TLS, ASN, geo, VPN detection, oracle benchmark ``` This architecture keeps raw signal data off the client. The browser only ever sees the verdict — not the evidence. This prevents fingerprint spoofing by making it impossible for the client to know exactly which signals are failing. ## Server-Side Signals In addition to the 41 client signals, FingerprintIQ extracts signals from the HTTP request at the Cloudflare edge. These are captured during the TLS handshake and TCP connection — before any JavaScript runs. They depend on the OS networking stack and TLS implementation, not on what the browser reports about itself. The three primary server-side signals are: - **JA4 TLS fingerprint** — derived from the ClientHello cipher suite ordering, extensions, and supported versions - **ASN classification** — whether the IP belongs to a residential ISP, mobile carrier, datacenter, VPN provider, or Tor exit node - **RTT geo coherence** — the round-trip time to the nearest Cloudflare PoP vs. the expected latency for the claimed IP location Every TLS connection begins with a ClientHello message containing cipher suite preferences, extension list, and protocol versions. This combination is characteristic of the client's TLS implementation — Chrome, Firefox, and Safari each produce distinct TLS fingerprints. FingerprintIQ captures the cipher suite, TLS version, HTTP protocol (HTTP/2 vs HTTP/3), ClientHello length, and JA4 hash. Every IP address belongs to an Autonomous System (AS). FingerprintIQ classifies each ASN into a category: Residential ISP, Mobile Carrier, Datacenter, Commercial VPN, Tor Exit Node, Education, Government, or CDN. Datacenter and Tor classifications significantly increase bot probability. Cross-validates the claimed IP location against network timing. The TCP round-trip time to the nearest Cloudflare colo is compared against the expected RTT based on the claimed city. A coherence score below 0.3 suggests a VPN or proxy is in use — the user's true location differs from their IP. ## Bot Detection The bot probability score (0.0 – 1.0) combines multiple weighted indicators. A score above 0.5 warrants a CAPTCHA challenge; above 0.7 warrants blocking. | Factor | Weight | Description | |--------|--------|-------------| | Datacenter ASN | +0.25 | Traffic from cloud providers (AWS, GCP, Azure) | | Tor Exit Node | +0.30 | Traffic routed through Tor | | Software Renderer | +0.25 | SwiftShader, LLVMpipe, Mesa — headless GPU | | Headless Markers | +0.35 | WebDriver flag, Puppeteer/Playwright signatures | | API Tampering | +0.20 | Modified browser APIs detected via integrity check | | Missing Signals | +0.30 | No canvas, WebGL, or audio — stripped APIs | | UA/TLS Mismatch | +0.30 | User-Agent doesn't match TLS fingerprint | Bot scores are additive and can exceed 1.0 before clamping. A headless browser running through a datacenter with a spoofed UA will score near 1.0 across all indicators simultaneously. --- # Introduction > FingerprintIQ is an open-source device fingerprinting and Web3 Sybil detection platform. Source: https://docs.fingerprintiq.com/introduction FingerprintIQ is now in public beta — 25,000 identifications/month free, forever. # FingerprintIQ FingerprintIQ identifies devices with **99%+ accuracy** using 41 browser signals, server-side TLS analysis, and Web3 wallet detection — all running on Cloudflare's global edge network. Get your first fingerprint in under 5 minutes See FingerprintIQ in action on your own device Full endpoint documentation with examples Retrieve full signal data server-side via the Events API All 41 signals explained with entropy scores ## Products FingerprintIQ is three products that work independently or together. **Identify** — browser-side device fingerprinting. Install `@fingerprintiq/js`, call `fiq.identify()`, get a stable visitor ID that persists across cookie clearing, incognito mode, and IP changes. 41 client signals fused with server-side TLS and network analysis. The identify response returns verdicts and scores; retrieve full signal data server-side via the Events API (`GET /v1/events/:requestId`). Free tier: 25,000 identifications/month. **Sentinel** — server-side caller classification. Add the `@fingerprintiq/server` middleware to any Hono (or Node.js) API and know whether each caller is a real browser, an AI agent, a CLI tool, or a scraper. No client SDK required. Free tier: 25,000 inspections/month. **Pulse** — CLI and agent analytics. Add `@fingerprintiq/pulse` to your CLI tool or AI agent to track command usage, machine counts, and version adoption without collecting PII. Free tier: 50,000 events/month. ## Why FingerprintIQ? Canvas, WebGL, WebGPU, audio, fonts, WASM timing, speech synthesis, timezone, CSS fingerprinting, JS error messages, behavioral risk, and more TLS fingerprint (JA4), ASN classification, geo coherence via RTT — signals the browser cannot fake Retrieve full signal data for any identification event via `GET /v1/events/:requestId` — raw signals never leave your server Wallet extension detection (MetaMask, Phantom, Rabby, 15+ wallets) without connecting or requesting a signature Sub-50ms identification via Cloudflare Workers deployed globally across 300+ cities No PII stored, no cross-site tracking, GDPR compliant by design 25,000 identifications/month, forever — no credit card required ## How It Works Add the lightweight client SDK (under 18 KB gzipped) to your site with a single npm install or CDN script tag. The SDK collects 41 browser signals in parallel — canvas rendering, WebGL GPU info, audio context fingerprint, installed fonts, Web3 wallets, and more. Signals are sent to the FingerprintIQ edge API on Cloudflare Workers, which adds server-side signals (TLS fingerprint, ASN classification, geo coherence) and computes a stable visitor ID. The same device returns the same visitor ID across sessions, even if the user clears cookies, uses incognito mode, changes IP, or switches networks. FingerprintIQ uses a two-tier approach: a stable core hash from 7 high-entropy signals, plus a fuzzy similarity score from supporting signals. This means minor browser updates or extension changes don't break identification. See [How It Works](/how-it-works) for the full technical breakdown. ## Pricing | Plan | Price | Identifications | Features | |------|-------|----------------|----------| | Free | $0/mo | 25,000/mo | All signals, dashboard, 1 API key | | Builder | $19/mo | 50,000/mo | Webhooks, priority support | | Growth | $49/mo | 250,000/mo | Team access, advanced analytics | | Scale | $149/mo | 2,000,000/mo | SSO, SLA, dedicated support | ## Frequently Asked Questions No. FingerprintIQ stores a hashed device fingerprint and visit metadata (timestamp, country, bot score) — never names, email addresses, or any PII. The visitor ID is a random ULID with no connection to real-world identity. Incognito mode does not change hardware signals (GPU, CPU, screen), rendering outputs (canvas, WebGL, audio), or system data (fonts, speech voices, timezone). FingerprintIQ re-identifies incognito sessions with the same accuracy as regular sessions. Determined users can block any fingerprinting service. FingerprintIQ includes resistance detection to flag privacy browsers (Brave, Tor, Firefox RFP) and extensions (CanvasBlocker, JShelter). Blocked or degraded signals reduce confidence scores rather than breaking identification entirely. FingerprintIQ is open-source, runs on your own Cloudflare account (no data leaving your infrastructure), includes Web3 wallet detection natively, and costs significantly less. The free tier is more generous. FingerprintIQ processes device characteristics, not personal data. No cookies are set, no cross-site tracking occurs, and no data is shared with third parties. Consult your legal counsel for your specific use case and jurisdiction. --- # Quick Start > Get your first device fingerprint in under 5 minutes. Source: https://docs.fingerprintiq.com/quickstart ## 1. Get Your API Key Sign up at [fingerprintiq.com](https://fingerprintiq.com/login) and create an API key from the dashboard. Use a `fiq_test_` key during development. Test keys have the same full functionality but are excluded from your monthly quota. ## 2. Install the SDK ```bash npm npm install @fingerprintiq/js ``` ```bash yarn yarn add @fingerprintiq/js ``` ```bash pnpm pnpm add @fingerprintiq/js ``` ```html CDN ``` This guide covers **Identify** (`@fingerprintiq/js`), the browser fingerprinting product. If you want server-side caller classification, see the [Sentinel guide](/guides/sentinel) (`@fingerprintiq/server`). For CLI and agent analytics, see the [Pulse guide](/guides/pulse) (`@fingerprintiq/pulse`). ## 3. Identify a Visitor ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_your_api_key_here', }); const result = await fiq.identify({ tag: { page: 'checkout' }, linkedId: 'user_123', }); console.log(result.visitorId); // "iq_01hns3k6tez83695a6t714s6n1" console.log(result.requestId); // "req_01hns3k6tez83695a6t7" console.log(result.confidence); // 1.0 console.log(result.botProbability); // 0.05 console.log(result.suspectScore); // 0 console.log(result.visitCount); // 3 ``` The SDK collects all 41 client signals in parallel before sending a single request. The edge worker adds server-side signals (JA4 TLS fingerprint, ASN, RTT) before computing the visitor ID. Total collection time is typically 50–150 ms depending on device speed. ## 4. Inspect the Response ```json { "requestId": "req_01hns3k6tez83695a6t7", "visitorId": "iq_01hns3k6tez83695a6t714s6n1", "confidence": 1.0, "botProbability": 0.05, "suspectScore": 0, "visitCount": 3, "firstSeenAt": 1712000000000, "lastSeenAt": 1712200000000, "riskFactors": [], "verdicts": { "bot": { "result": false, "probability": 0.05 }, "vpn": { "result": false, "confidence": 0.92 }, "tor": { "result": false }, "proxy": { "result": false }, "incognito": { "result": false }, "tampering": { "result": false, "anomalyScore": 0 }, "headless": { "result": false }, "virtualMachine": { "result": false }, "devtools": { "result": false }, "privacyBrowser": { "result": false, "name": null }, "highActivity": { "result": false }, "ipBlocklist": { "result": false } }, "ip": "203.0.113.42", "ipLocation": { "country": "US", "city": "New York", "region": "NY", "latitude": 40.71, "longitude": -74.01 }, "timestamp": 1712000003000 } ``` The same `visitorId` persists across sessions, incognito mode, cookie clearing, and IP changes. Try it on the [live demo](https://fingerprintiq.com/demo). ## 5. Verify Server-Side (Recommended) After identifying on the client, pass the `requestId` to your backend and look up the full event via the Server API. This gives you complete signal data and prevents client-side spoofing. ```typescript // Your backend (Node.js, Hono, Next.js API route, etc.) const res = await fetch( `https://fingerprintiq.com/v1/events/${result.requestId}`, { headers: { 'Authorization': 'Bearer fiq_secret_your_key' } } ); const event = await res.json(); // Full signals available in event.signals.client and event.signals.server const isLegitimate = event.confidence >= 0.8 && event.botProbability < 0.3 && !event.verdicts.bot.result; ``` Never make trust decisions based solely on client-side data. Always verify the `visitorId` on your server before granting access or taking action. ## Next Steps Error handling, use cases, and full configuration for vanilla JS React hooks and component patterns for SPAs Validate fingerprints on your backend securely Retrieve full signal data via GET /v1/events/:requestId Understand all 41 signals and their entropy scores --- # Response Caching > Cache identification results to reduce API calls on SPAs with frequent navigation. Source: https://docs.fingerprintiq.com/sdk/caching The FingerprintIQ SDK includes a built-in response cache. When enabled, repeated `identify()` calls within the TTL window return the cached result immediately — no network request is made. ## When to use caching **Use caching when:** - Your app is a single-page application (SPA) where users navigate between views without a full page reload - You call `identify()` on every route change and the device identity is unlikely to have changed in seconds - You want to reduce API call volume to stay within your monthly quota **Do not use caching when:** - You need fresh bot/velocity data on every page load — for example, at a checkout gate or login step where a bot could have changed state between navigations - Your TTL would cause you to miss a VPN or proxy being activated mid-session - You are making a security-critical decision and need the most recent signal snapshot Cached responses replay the original verdict as of the time of first collection. If a user activates a VPN or switches from a datacenter to a residential IP after the cache was populated, you will not see the new signals until the TTL expires. For high-stakes decisions, always call `identify()` with caching disabled or with a short TTL. --- ## Configuration Pass `cache` options to the `FingerprintIQ` constructor. Where to store the cached response. - `"sessionStorage"` — cache survives page navigations within the tab but is cleared when the tab closes. Recommended for most SPAs. - `"localStorage"` — cache persists across tabs and browser restarts until the TTL expires. - `"memory"` — cache lives only in the current JavaScript heap. Cleared on full page reload. Useful for server-side rendering contexts. Defaults to `"sessionStorage"`. Time-to-live in seconds. After this many seconds, the cached entry is treated as stale and a fresh `identify()` call is made on the next invocation. Defaults to `3600` (1 hour). ### Example: basic cache setup ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', cache: { storage: 'sessionStorage', ttl: 3600, // 1 hour }, }); // First call: makes a network request, stores result const result1 = await fiq.identify(); console.log(result1.cacheHit); // false // Second call within TTL: returns cached result, no network const result2 = await fiq.identify(); console.log(result2.cacheHit); // true ``` ### Example: short TTL for higher-stakes pages ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', cache: { storage: 'sessionStorage', ttl: 60, // 1 minute — fresh data for sensitive flows }, }); ``` ### Example: localStorage for persistent caching ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', cache: { storage: 'localStorage', ttl: 7200, // 2 hours, survives tab close }, }); ``` --- ## The `cacheHit` field When a response is served from cache, the result includes `cacheHit: true`. This field is injected by the SDK — it is not returned by the API. ```typescript interface IdentifyResponse { visitorId: string; // ... other fields ... cacheHit: boolean; // true if response came from cache } ``` You can use `cacheHit` to decide whether to refresh the fingerprint for a critical action: ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', cache: { storage: 'sessionStorage', ttl: 3600 }, }); async function identifyForCheckout() { const result = await fiq.identify(); // If we got a cached result, force a fresh identification for checkout if (result.cacheHit) { return fiq.identify({ skipCache: true }); } return result; } ``` --- ## Cache key The cache key is derived from your **API key prefix** (the first 16 characters of your `fiq_live_` or `fiq_test_` key). This means: - Different API keys use different cache buckets — switching between test and live keys won't serve stale results - All `identify()` calls from the same instance share the same cache entry - The full API key is never stored in sessionStorage or localStorage — only the prefix is used as the key name ``` fiq_cache_v1_fiq_live_abc12345 → { visitorId: "...", ttl: 1712003600000 } ``` --- ## Disabling the cache To bypass the cache for a single call without changing the constructor config, pass `skipCache: true`: ```javascript // Always get fresh data for this specific call const freshResult = await fiq.identify({ skipCache: true }); console.log(freshResult.cacheHit); // always false ``` To disable caching entirely, omit the `cache` option from the constructor (the default behavior is no caching). --- ## Usage in React In a React SPA, instantiate once outside the component tree and rely on the cache for repeated route-change identifies: ```tsx // lib/fingerprint.ts export const fiq = new FingerprintIQ({ apiKey: import.meta.env.VITE_FINGERPRINT_KEY, cache: { storage: 'sessionStorage', ttl: 1800, // 30 minutes }, }); ``` ```tsx // hooks/useFingerprint.ts export function useFingerprint() { const [result, setResult] = useState(null); useEffect(() => { fiq.identify().then(setResult).catch(console.error); }, []); // called once per mount, cache handles re-renders return result; } ``` Because the SDK instance is created outside React's render cycle, the `FingerprintIQ` object is never re-created on re-renders. The cache is shared across all components that import from `lib/fingerprint.ts`. --- # SDK Configuration > Configure the FingerprintIQ SDK for your use case. Source: https://docs.fingerprintiq.com/sdk/configuration ## All Options ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_your_key', // Required endpoint: 'https://fingerprintiq.com', // Optional timeout: 10000, // Optional, ms detectWallets: true, // Optional cache: { // Optional storage: 'sessionStorage', ttl: 3600, }, }); ``` ## Option Reference Your API key from the FingerprintIQ dashboard. The only required option. - `fiq_live_*` — Production key. Counts toward your monthly quota. - `fiq_test_*` — Development key. Full functionality, excluded from quota. The API endpoint to send identification requests to. Defaults to `https://fingerprintiq.com`. Override this to route traffic through your own domain (useful for avoiding ad blockers or maintaining a first-party data relationship). Maximum time in milliseconds to wait for the identification request to complete. Defaults to `10000` (10 seconds). This covers both signal collection time (~50–150 ms) and the network round-trip. Only reduce this below 5000 ms if you're certain your users have fast, reliable connections. Whether to enumerate Web3 wallet extensions. Defaults to `true`. Wallet detection adds approximately 50 ms to the collection phase. Disable it on non-Web3 sites or performance-critical pages where wallet data is not needed. Caching configuration to avoid redundant identifications within a session or across page loads. - `storage` — Where to persist the cached result: `"sessionStorage"` (default, cleared when tab closes), `"localStorage"` (persists across sessions), or `"memory"` (cleared on page reload). - `ttl` — Time-to-live in seconds. After expiry, the next `identify()` call makes a fresh API request. Defaults to `3600` (1 hour) when `cache` is configured. When a cached result is returned, `identify()` resolves immediately without collecting signals or making a network request. The cached `requestId` and `visitorId` remain valid for server-side lookups. ## Common Configurations ### Production (default) ```typescript const fiq = new FingerprintIQ({ apiKey: process.env.FIQ_API_KEY, }); ``` ### Custom Proxy Endpoint Route FingerprintIQ traffic through your own domain to avoid ad blockers: ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', endpoint: 'https://fp.yoursite.com', // proxy to fingerprintiq.com }); ``` A Cloudflare Worker is an ideal proxy — it adds zero latency if deployed on the same network as the FingerprintIQ edge API. ### Disable Wallet Detection For non-Web3 sites where wallet data is never used: ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', detectWallets: false, // saves ~50ms }); ``` ### Extended Timeout for Slow Networks For mobile-first applications or regions with high latency: ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', timeout: 20000, // 20 seconds }); ``` The timeout applies to the entire identify() call including signal collection. Most of the time budget is consumed by the audio signal (~40–80 ms) and the network round-trip. ### Session Caching Cache results for a session to avoid redundant identifications on repeat page views: ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', cache: { storage: 'sessionStorage', ttl: 3600, // 1 hour }, }); ``` For single-page apps where the visitor persists across route changes, use `memory` storage to avoid any serialization overhead: ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...', cache: { storage: 'memory', ttl: 1800, // 30 minutes }, }); ``` --- # JavaScript SDK Reference > Complete API reference for @fingerprintiq/js. Source: https://docs.fingerprintiq.com/sdk/javascript ## Class: FingerprintIQ ### Constructor ```typescript new FingerprintIQ(config: FingerprintIQConfig) ``` Your API key from the dashboard. Use `fiq_live_` for production and `fiq_test_` for development. Test keys have full functionality and don't count toward your monthly quota. API endpoint URL. Defaults to `https://fingerprintiq.com`. Override for custom proxy or self-hosted deployments. Request timeout in milliseconds. Defaults to `10000` (10 seconds). Signal collection itself takes 50–150 ms; the rest of the timeout budget covers the network round-trip. Enable Web3 wallet detection. Defaults to `true`. Set to `false` to skip wallet enumeration and save approximately 50 ms of collection time on non-Web3 sites. ### Methods #### `identify(options?: IdentifyOptions): Promise` Collects all configured signals in parallel and sends them to the FingerprintIQ API. Returns the compact identification result including the stable `visitorId`, `requestId`, verdicts, and scores. Full signal data is available server-side via the Events API using `result.requestId`. ```typescript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...' }); // Basic usage const result = await fiq.identify(); console.log(result.visitorId); // "iq_01hns3k6tez83695a6t714s6n1" console.log(result.requestId); // "req_01hns3k6tez83695a6t7" console.log(result.visitCount); // 3 console.log(result.botProbability); // 0.05 console.log(result.suspectScore); // 0 // With metadata and per-call options const result = await fiq.identify({ tag: { page: 'checkout', experiment: 'v2' }, linkedId: 'user_123', timeout: 5000, }); ``` **Options:** Arbitrary key-value metadata to attach to this event. Stored with the event and accessible via the Server API. Keys and values must be strings. A string identifier to link this event to an authenticated entity (user ID, session ID, order ID). Enables querying all events for a specific entity via the Server API. Per-call timeout override in milliseconds. Overrides the instance-level `timeout` for this call only. **Throws:** - `Error` if the API returns a non-200 status (message includes the HTTP status code) - `Error` if the request exceeds the configured `timeout` - `Error` if `apiKey` is not provided in the constructor config The `FingerprintIQ` instance is lightweight and safe to reuse. Instantiate it once at module scope and call `identify()` whenever you need a fingerprint — don't create a new instance on each call. ## Types ### FingerprintIQConfig ```typescript interface FingerprintIQConfig { apiKey: string; endpoint?: string; // default: 'https://fingerprintiq.com' timeout?: number; // default: 10000 (ms) detectWallets?: boolean; // default: true cache?: { storage: 'sessionStorage' | 'localStorage' | 'memory'; ttl: number; // seconds }; } ``` ### IdentifyOptions ```typescript interface IdentifyOptions { tag?: Record; // Arbitrary metadata for this event linkedId?: string; // Entity ID to link this event to timeout?: number; // Per-call timeout override (ms) } ``` ### IdentifyResponse ```typescript interface IdentifyResponse { requestId: string; // Unique event identifier (format: req_) visitorId: string; // Stable device identifier (format: iq_) confidence: number; // 0.0 - 1.0 signal confidence botProbability: number; // 0.0 - 1.0 bot likelihood suspectScore: number; // 0 - 100 composite risk score visitCount: number; // Total visits from this device firstSeenAt: number; // Unix timestamp (ms) of first identification lastSeenAt: number; // Unix timestamp (ms) of previous identification verdicts: { bot: { result: boolean; probability: number }; vpn: { result: boolean; confidence: number }; tor: { result: boolean }; proxy: { result: boolean }; incognito: { result: boolean }; tampering: { result: boolean; anomalyScore: number }; headless: { result: boolean }; virtualMachine: { result: boolean }; devtools: { result: boolean }; privacyBrowser: { result: boolean; name: string | null }; highActivity: { result: boolean }; ipBlocklist: { result: boolean }; }; ip: string; // Client IP address ipLocation: { country: string; city: string; region: string; latitude: number; longitude: number; }; riskFactors: string[]; // Active risk indicators timestamp: number; // Unix timestamp (ms) of this visit } ``` ## Bundle Size The SDK is designed to be lightweight. Signal collection code is bundled inline — no dynamic imports or external dependencies. | Build | Raw Size | Gzipped | |-------|----------|---------| | ESM | 37.7 KB | ~12 KB | | CJS | 38.1 KB | ~12 KB | ## Browser Support | Browser | Minimum Version | |---------|----------------| | Chrome | 80+ | | Firefox | 78+ | | Safari | 14+ | | Edge | 80+ | | Opera | 67+ | Older browsers that lack specific APIs (e.g., OfflineAudioContext, WebGL) will return `null` for those signals rather than throwing. The core fingerprint remains functional with reduced signal coverage. --- # Signal Types Reference > TypeScript type definitions for all 29 signal collectors. Source: https://docs.fingerprintiq.com/sdk/signals-reference ## Signal Result Wrapper Every signal is wrapped in a `SignalResult`. Signals that fail to collect (due to blocked APIs, browser restrictions, or errors) return `null` rather than throwing. ```typescript interface SignalResult { value: T; // The signal data duration: number; // Collection time in milliseconds } ``` All signal values in the `/v1/identify` request body are `SignalResult` objects. The `duration` field is used internally for performance monitoring and to detect artificially slow signal collection (which can indicate automation). ## Signal Type Definitions **CanvasSignal** ```typescript interface CanvasSignal { hash: string; // SHA-256 of the rendered canvas pixel data isFarbled: boolean; // true if Brave canvas farbling was detected } ``` **WebGLSignal** ```typescript interface WebGLSignal { renderer: string; // GPU model (e.g., "ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Max)") vendor: string; // GPU vendor string extensions: string[]; // Supported WebGL extensions params: Record; // GL parameters (MAX_TEXTURE_SIZE, etc.) gpuTimingMs: number | null; // GPU benchmark timing in ms isSoftwareRenderer: boolean; // true if SwiftShader/LLVMpipe/Mesa detected } ``` **AudioSignal** ```typescript interface AudioSignal { hash: string; // SHA-256 of the first 100 rendered audio buffer samples sampleRate: number; // AudioContext sample rate (e.g., 44100) maxChannelCount: number; // Max output channels isSuspended: boolean; // true if AudioContext is in suspended state } ``` **NavigatorSignal** ```typescript interface NavigatorSignal { hardwareConcurrency: number; // Logical CPU core count deviceMemory: number | null; // RAM in GB (Chrome only, rounded to nearest power of 2) maxTouchPoints: number; // 0 for non-touch devices languages: string[]; // e.g., ["en-US", "en"] platform: string; // e.g., "MacIntel", "Win32" cookieEnabled: boolean; doNotTrack: string | null; keyboardLayout: string | null; connectionType: string | null; // e.g., "wifi", "4g" hasBluetooth: boolean; hasUsb: boolean; hasHid: boolean; hasSerial: boolean; hasWakeLock: boolean; hasGpu: boolean; bluetoothAvailable: boolean | null; } ``` **ScreenSignal** ```typescript interface ScreenSignal { width: number; height: number; availWidth: number; // Usable width minus taskbar availHeight: number; // Usable height minus taskbar colorDepth: number; // Bits per color component pixelRatio: number; // devicePixelRatio isRfpRounded: boolean; // true if Firefox RFP rounding detected } ``` **MathSignal** ```typescript interface MathSignal { hash: string; // Hash of computed Math function outputs values: { sin: number; cos: number; tan: number; log: number; sqrt: number; asin: number; atan: number; exp: number; }; } ``` **ErrorSignal** ```typescript interface ErrorSignal { messages: string[]; // 9 engine-specific error message strings hash: string; // SHA-256 of concatenated error messages } ``` **SpeechSignal** ```typescript interface SpeechSignal { voiceCount: number; // Total installed TTS voices localVoices: string[]; // Local (offline) voice names remoteVoiceCount: number; // Remote (online) voice count defaultVoice: string | null; // Default voice name hash: string; // SHA-256 of voice list } ``` **FontsSignal** ```typescript interface FontsSignal { count: number; // Number of detected fonts out of ~650 tested detectedFonts: string[]; // Names of detected font families } ``` **TimezoneSignal** ```typescript interface TimezoneSignal { timezone: string; // IANA timezone (e.g., "America/New_York") offset: number; // UTC offset in minutes isSpoofed: boolean; // true if timezone inconsistency detected historicalOffset: number; // Offset 6 months ago (DST cross-check) } ``` **WalletSignal** ```typescript interface WalletSignal { detected: string[]; // Wallet names found (e.g., ["MetaMask", "Phantom"]) count: number; // Total wallets installed evmProviders: string[]; // EVM-compatible wallet names solanaProviders: string[]; // Solana wallet names multipleWallets: boolean; // true if more than one wallet detected versions: Record; // Wallet name → version string } ``` **IntegritySignal** ```typescript interface IntegritySignal { lieScore: number; // Count of tampered/faked APIs detected tamperedApis: string[]; // Names of APIs that failed toString() checks workerMismatch: boolean; // true if worker scope values differ from main thread } ``` **HeadlessSignal** ```typescript interface HeadlessSignal { isHeadless: boolean; // true if any headless markers found webdriver: boolean; // navigator.webdriver flag markers: string[]; // Specific markers detected (e.g., "PUPPETEER", "PLAYWRIGHT") } ``` **ResistanceSignal** ```typescript interface ResistanceSignal { isBrave: boolean; isTorBrowser: boolean; isFirefoxRfp: boolean; privacyExtensions: string[]; // Detected extension names timerPrecision: number; // Measured timer precision in ms } ``` See the full TypeScript definitions in [`src/types.ts`](https://github.com/fingerprintiq/js/blob/main/src/types.ts) for all 29 signal interfaces, including less common signals like `WebGPUSignal`, `WasmTimingSignal`, `DOMRectSignal`, `IntlSignal`, `SVGSignal`, and `CodecSignal`. --- # Tags & Linked IDs > Attach metadata and user identifiers to identification events. Source: https://docs.fingerprintiq.com/sdk/tag-linkedid Tags and linked IDs let you annotate identification events with your own data so you can correlate fingerprint results with the rest of your system — without modifying your database schema or adding extra lookups. ## tag A `tag` is arbitrary metadata you attach to an identification event. It is stored alongside the event and returned verbatim in Server API responses and webhook payloads. Any JSON-serializable value. Maximum size: 16 KB. Stored with the event and returned in `GET /v1/events/:requestId` and webhook payloads. Tags are useful for: - **Page context** — record which page or funnel step triggered identification - **Session metadata** — attach A/B test variant, feature flag state, or user-facing experiment ID - **Internal routing** — include a tenant ID or environment name for multi-tenant apps - **Debugging** — add a deployment version or correlation ID to trace issues across systems ### Example: tagging with page context ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...' }); const result = await fiq.identify({ tag: { page: 'checkout', step: 'payment', experimentVariant: 'B', appVersion: '2.4.1', }, }); ``` ### Example: scalar tag ```javascript // A simple string is valid too const result = await fiq.identify({ tag: 'checkout', }); ``` The tag is not interpreted or indexed by FingerprintIQ — it is stored and returned as-is. To query events by tag content you would filter on the server side after fetching via the Events API. --- ## linkedId A `linkedId` links an identification event to a known user, account, or entity in your system. Unlike `tag`, `linkedId` is indexed and **searchable** via the Server API. A string identifier from your system (e.g., a user ID, account ID, or session token). Maximum length: 256 characters. URL-safe characters recommended. Linked IDs are useful for: - **Multi-accounting detection** — find all `visitorId` values associated with a single `linkedId`, or all `linkedId` values associated with a single `visitorId` - **Audit trails** — look up every identification event that touched a specific account - **Chargeback investigations** — retrieve the full device history for an account at dispute time - **Velocity checks** — count how many distinct devices have claimed the same account Do not use PII (email addresses, phone numbers, names) as a `linkedId`. Use an opaque internal ID such as your database primary key or a UUID. ### Example: identify with linkedId ```javascript const fiq = new FingerprintIQ({ apiKey: 'fiq_live_...' }); // After the user logs in, link events to their account const result = await fiq.identify({ linkedId: currentUser.id, // e.g. "user_01hns3k6te" }); ``` ### Example: combining tag and linkedId ```javascript const result = await fiq.identify({ linkedId: currentUser.id, tag: { page: 'account-settings', action: 'password-change', }, }); ``` --- ## Querying by linkedId via Server API Use `GET /v1/visitors` with the `linkedId` query parameter to fetch all events associated with a specific identifier. ```bash curl "https://fingerprintiq.com/v1/visitors?linkedId=user_01hns3k6te&limit=50" \ -H "Authorization: Bearer fiq_secret_your_key_here" ``` ### Response ```json { "visitorId": "iq_01hns3k6tez83695a6t714s6n1", "visits": [ { "requestId": "req_01hns3k6tez83695a6t7", "linkedId": "user_01hns3k6te", "tag": { "page": "checkout" }, "timestamp": 1712000003000, "confidence": 0.97, "botProbability": 0.05, "ip": "203.0.113.42", "country": "US" } ], "pagination": { "limit": 50, "skip": 0, "total": 3 } } ``` ### Server-side: detect multi-accounting ```typescript // In your backend — check how many distinct visitorIds // are linked to a single user account async function checkForMultiAccounting(userId: string): Promise { const response = await fetch( `https://fingerprintiq.com/v1/visitors?linkedId=${userId}&limit=100`, { headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}`, }, } ); const data = await response.json(); // Count distinct visitorIds across all events for this linkedId const distinctDevices = new Set(data.visits.map((v: any) => v.visitorId)); // Flag if a single account has been accessed from more than 3 devices return distinctDevices.size > 3; } ``` ### Server-side: find all accounts on a device ```typescript // Check how many linkedIds (accounts) a single device has accessed async function getAccountsForDevice(visitorId: string): Promise { const response = await fetch( `https://fingerprintiq.com/v1/visitors/${visitorId}?limit=200`, { headers: { Authorization: `Bearer ${process.env.FINGERPRINT_SECRET_KEY}`, }, } ); const data = await response.json(); const linkedIds = data.visits .map((v: any) => v.linkedId) .filter(Boolean); return [...new Set(linkedIds)]; // deduplicated list of accounts } ``` --- ## Tag vs linkedId at a glance | | `tag` | `linkedId` | |---|---|---| | Type | Any JSON value | String only | | Max size | 16 KB | 256 chars | | Indexed | No | Yes | | Queryable | No (filter client-side) | Yes (`?linkedId=`) | | Use case | Contextual metadata | Linking to your user model | Use `linkedId` for anything you'll query — user IDs, account IDs, session tokens. Use `tag` for everything else — page names, experiment variants, debug context. Tags are free-form and never affect query performance. --- # Client Signals > Detailed reference for all 41 client-side signals collected by the FingerprintIQ SDK. Source: https://docs.fingerprintiq.com/signals/client-signals The FingerprintIQ SDK collects 41 signals across six categories, running all collectors in parallel to minimize latency. Each signal is wrapped in a `SignalResult` containing the value and collection duration in milliseconds. Signals derived from how the browser renders graphics and text. These are highly device-specific because they depend on GPU drivers, font renderers, and anti-aliasing implementations. **Canvas** — Renders a complex scene (gradients, text, shapes, emoji) on a 2D canvas and hashes the pixel output. Different GPU drivers, font renderers, and anti-aliasing engines produce unique results. - Entropy: High | Stability: Medium - Renders 3x and checks consistency to detect Brave's canvas farbling - Skipped on Safari 17+ and Firefox 120+ which inject rendering noise **WebGL** — Queries GPU hardware information via `WEBGL_debug_renderer_info`: - Unmasked renderer string (full GPU model) - Unmasked vendor - 5 GL parameters (MAX_TEXTURE_SIZE, etc.) - GPU timing via fragment shader benchmark - Software renderer detection (SwiftShader, LLVMpipe, Mesa) - Entropy: Very High | Stability: Very High **WebGPU** — Queries the WebGPU adapter for architecture, description, device, and vendor strings. Available in Chrome 113+ and Edge 113+. - Entropy: High | Stability: Very High **Audio** — Renders audio through an `OfflineAudioContext` with oscillator and compressor pipeline. Hashes the first 100 float samples from the rendered buffer. Different audio stacks produce unique waveforms. - Entropy: High | Stability: Medium **SVG Text** — Creates SVG text elements with emoji content and measures via `getBBox()`, `getComputedTextLength()`, and `getSubStringLength()`. Independent from Canvas rendering, different font shaping engines produce different measurements. - Entropy: Medium | Stability: High **DOMRect** — Measures `getBoundingClientRect()` precision and emoji rendering dimensions. Sub-pixel differences reveal browser engine version and font rendering settings. - Entropy: Medium | Stability: High Signals derived from device hardware characteristics. These are among the most stable signals — hardware rarely changes. **Navigator** — System properties that identify the device: - `hardwareConcurrency` (CPU core count) - `deviceMemory` (RAM in GB, Chrome only) - `maxTouchPoints` (0 for non-touch devices) - `platform`, `languages`, `cookieEnabled` - Keyboard layout, connection type - Device capabilities: Bluetooth, USB, HID, Serial - Entropy: Medium | Stability: Very High **Screen** — Display characteristics: - Resolution (width × height) - Available area (minus taskbar/dock) - Color depth and pixel ratio - Firefox RFP rounding detection - Entropy: Medium | Stability: Very High **Platform Features** — Tests 15+ APIs to estimate the platform: - BarcodeDetector, ContactsManager (Android-specific) - HID, Serial, USB (desktop-only) - SharedWorker, PointerEvent, TouchEvent - Cross-validates against claimed platform string - Entropy: High | Stability: Very High **WASM Timing** — Executes a minimal WebAssembly module and measures execution time over 10 iterations. The median and standard deviation create a CPU microarchitecture fingerprint that differs between Intel, AMD, and Apple Silicon. - Entropy: Medium | Stability: Medium Signals derived from JavaScript and CSS engine behavior. These reveal the exact browser version and engine used. **Math Precision** — JavaScript engines (V8, SpiderMonkey, JavaScriptCore) compute `Math.sin`, `Math.tan`, `Math.log` with subtly different floating-point precision. The hash of computed values reliably identifies the engine. - Entropy: Medium | Stability: Very High **JS Error Messages** — Deliberately triggers 9 JavaScript errors and captures the exact error message text. Error wording differs between engine versions and is nearly impossible to spoof without patching the engine binary. - Entropy: High | Stability: Very High **CSS Computed Style** — Three sub-signals: - Counts `getComputedStyle` property count (identifies browser version precisely) - Reads 19 CSS system colors (reveals OS theme: light/dark) - Reads 6 system fonts (reveals OS font configuration) - Entropy: High | Stability: High **Window Features** — Counts `Object.getOwnPropertyNames(window)` and categorizes by prefix (webkit/moz). Detects "client litter" — non-standard properties injected by browser extensions. - Entropy: Medium | Stability: High **HTML Element Properties** — Walks the prototype chain of `document.documentElement` collecting all property names. The full set differs between browser versions. - Entropy: Medium | Stability: High Signals derived from the operating system configuration. These are highly specific to the user's locale, installed software, and system settings. **Speech Synthesis** — Enumerates `speechSynthesis.getVoices()`. The installed TTS voice list is OS and locale-specific. An en-US macOS device has completely different voices than an en-US Windows device. - Entropy: Very High | Stability: High **Intl Locale** — Tests all 7 Intl constructors (`DateTimeFormat`, `NumberFormat`, `Collator`, etc.) and fingerprints their formatted output. Detects locale spoofing by comparing against `navigator.language`. - Entropy: High | Stability: Very High **Timezone (Deep)** — Goes beyond basic UTC offset: - Historical offset calculation across DST boundaries - Cross-validation of reported vs computed timezone name - Timezone spoofing detection - Entropy: Medium | Stability: Very High **Media Codecs** — Tests 12 audio/video MIME types across `canPlayType()`, `MediaSource.isTypeSupported()`, and `MediaRecorder.isTypeSupported()`. Codec support differs by OS, browser, and hardware. - Entropy: High | Stability: High **Fonts** — Measures text width against ~650 font families using sub-pixel precision `getBoundingClientRect()`. The set of installed fonts differs substantially between operating systems and user configurations. - Entropy: High | Stability: High Signals specifically designed to detect bots, headless browsers, privacy tools, and API tampering. **Integrity** — Detects API tampering via `Function.prototype.toString.call()` checks on 30+ browser APIs. Computes a "lie score" from detected fake implementations. Compares main thread vs Web Worker values to catch inconsistencies. - Entropy: Medium | Stability: High **Worker Scope Cross-Validation** — Spawns a Web Worker and collects navigator and WebGL data independently. Most spoofing tools only modify the main window object — worker scope reveals true underlying values. - Entropy: Medium | Stability: High **Headless Detection** — Checks for automation markers: - WebDriver flag (`navigator.webdriver`) - Puppeteer/Playwright/Selenium signatures - Missing or fake Chrome runtime object - Zero plugins in a non-private session - Headless User-Agent substrings - Entropy: High | Stability: High **Resistance (Privacy Tools)** — Detects privacy browsers (Brave, Tor Browser, Firefox RFP) and extensions (CanvasBlocker, JShelter, DuckDuckGo Privacy Essentials, Trace). Measures timer precision reduction. - Entropy: Medium | Stability: Medium **Storage** — Tests localStorage, sessionStorage, IndexedDB, and cookie availability. Storage quota is reduced in incognito mode — detects private browsing. - Entropy: Low | Stability: High **Status** — Miscellaneous device signals: - Timer precision (reduced by Firefox RFP and some extensions) - Max call stack size (differs by engine version and device) - Storage quota (reduced in incognito) - Battery API availability - V8 heap size limit (Chrome/Edge only) - Entropy: Medium | Stability: Medium New signals in the expanded 41-signal suite covering additional device characteristics, behavioral patterns, and environment detection. **UA Client Hints** — Queries the `navigator.userAgentData` API (Chrome 90+) for structured OS and browser version data. Cross-validates against the legacy User-Agent string to detect inconsistencies. - Entropy: Medium | Stability: Very High **Capability Vector** — Tests a battery of browser API availability checks (Payment Request API, Credential Management API, Web Share API, etc.) and encodes the result as a compact binary vector. Distinguishes browser versions and platforms with high precision. - Entropy: Medium | Stability: High **Geometry Vector** — Measures layout geometry from multiple HTML elements (scrollbar width, zoom level, subpixel rendering) to build a rendering environment fingerprint. - Entropy: Medium | Stability: High **Runtime Vector** — Samples V8 runtime behavior: heap size, garbage collection timing hints, and Promise microtask scheduling order. Reveals hidden browser runtime characteristics. - Entropy: Medium | Stability: High **Sensor Capabilities** — Detects availability of hardware sensors (Accelerometer, Gyroscope, Magnetometer, AmbientLightSensor). Presence or absence is highly device-type-specific. - Entropy: Low | Stability: Very High **Behavioral Risk** — Analyzes interaction patterns leading up to the identification call: mouse movement entropy, keyboard event timing, touch pressure distribution. Low behavioral entropy is a strong bot signal. - Entropy: High | Stability: Medium **Incognito** — Combines storage quota, FileSystem API behavior, and browser-specific incognito detection heuristics to determine if the page is loaded in a private browsing session. - Entropy: Medium | Stability: High **DevTools** — Detects open developer tools via console output timing differences, `window.outerWidth` vs `window.innerWidth` discrepancy, and debugger statement timing. - Entropy: Medium | Stability: Low **Virtualization** — Detects virtual machines and sandboxed environments via GPU renderer strings, CPU topology inconsistencies, and timing anomalies characteristic of hypervisors. - Entropy: High | Stability: High **Rooted / Jailbreak** — Mobile-specific checks for signs of rooted Android or jailbroken iOS devices: non-standard API surfaces, file system access patterns, and modified user agent strings. - Entropy: Medium | Stability: High **CSS Feature Detection** — Tests support for bleeding-edge CSS features (container queries, `@layer`, `color-mix()`, `has()`) to fingerprint browser version with high precision beyond what User-Agent parsing provides. - Entropy: Medium | Stability: High **Frame Depth** — Detects whether the page is running inside an `