Most device identification products share a shape: a client agent identifies the visitor and returns a request ID, and your server reads the result with a secret key. Most of a migration is renaming. This guide maps each part, then walks through switching over without a gap in protection.
At a glance
| What you use today | In Fingerly |
|---|---|
| Load the client agent with a public key | load({ apiKey }) |
| Identify the visitor at a moment that matters | fingerly.identify() |
| A linked ID and a tag or custom data | One tag string |
| A visitor ID | visitorId, scoped to your organization |
| A match confidence, often 0 to 1 | visitorConfidence, 0 to 100 |
| Risk signals and a risk score | Signal groups, suspect_score and suspect_level |
| Read one result on your server | GET /api/v1/events/{request_id} |
| A visitor's history | GET /api/v1/events?visitor=…, 30 days |
| A webhook per identification | identification.completed, and visitor.suspect with signals |
| A region option | None. One API, one origin |
| A custom subdomain or a cloud proxy | Proxy integrations, or a hosted subdomain as a fallback |
Plan the switch
Step 1: Create Fingerly keys
Create a public key with your site's origins and a secret key, first in development, then in production. Allowed origins are required, not optional: a public key with none refuses every browser request. See API keys.
Step 2: Run both agents side by side
Call Fingerly next to your current agent at the same moments and send both request IDs to your server. Keep deciding with your current provider, and log what Fingerly would have decided.
Step 3: Move server checks
When the two agree on the sessions you care about, switch your decisions to the Fingerly event. Keep reading your current provider's results for a few days as a fallback.
Step 4: Move webhooks
Register Fingerly endpoints, verify their signatures, and point your consumers at them.
Step 5: Remove the old agent
Delete the old agent, its proxy routes and its keys.
Replace the JavaScript agent
import { load } from '@fingerly/web-js'
const fingerly = await load({
apiKey: 'fly_pk_production_…',
endpoints: '/k7x2qm4a',
fallbackToDefaultEndpoint: true,
consent: 'granted', // only once you have consent (or another lawful basis)
})
const { requestId, visitorId } = await fingerly.identify({ tag: 'login' })
Load options
| What you set today | Fingerly | Notes |
|---|---|---|
| A public API key | apiKey | A Fingerly public key, fly_pk_…. |
| A region | None | Fingerly is one API at one origin. There is nothing to choose. |
| Custom endpoints | endpoints | The path the dashboard issued for a proxy, such as /k7x2qm4a, or a URL. Posted to exactly as given, with nothing appended. |
| A fallback to the provider's own endpoint | fallbackToDefaultEndpoint: true | Try the Fingerly API after your own endpoints fail. |
| A script URL to proxy | None | The SDK is bundled into your application. Nothing is downloaded at runtime, so there is no script to proxy. |
| Nothing | consent | Defaults to pending: nothing is collected until it is granted. Pass granted once you have consent or another lawful basis, or call setConsent() from your consent tool. See privacy and consent. |
Identify options and result
| What you use today | Fingerly | Notes |
|---|---|---|
| A linked ID | Part of tag | Fingerly has one reference field. Put the account or order in it, such as login or checkout:8412. |
| A tag of any JSON | tag (a string) | Encode what you need as a string. Your server compares it before trusting the result. |
| An extended result | None | Network and device details are on the stored event your server reads. |
| A timeout | signal | Pass an AbortSignal. Collection is already capped at 300 ms, and each attempt at 5 seconds. |
| The request ID | requestId | Send it to your server with the action. |
| The visitor ID | visitorId | Twenty letters and digits, scoped to your organization. |
| A confidence from 0 to 1 | visitorConfidence | 0 to 100. See visitor confidence. |
| Whether the visitor was found before | visitorIsNew | Usually the opposite sense. |
| An encrypted or sealed result | None | Read the event on your server with a secret key instead. |
Fingerly also returns the score, the level and the signals in the same response, and local verdicts computed in the browser. Use them to adapt the interface; decide on your server.
Replace a React library
import { FingerlyProvider, useIdentify } from '@fingerly/react'
<FingerlyProvider apiKey="fly_pk_production_…" consent="granted">
<App />
</FingerlyProvider>
const { identify, isLoading } = useIdentify({ tag: 'login' })
const { requestId } = await identify({ force: true })
useIdentify({ immediate: true }) identifies on mount, and refresh() identifies again, bypassing any cached result. Vue, Svelte, Angular and Next.js have the same shape: see the SDKs.
Mobile
On iOS and Android, replace your current client and its visitor ID calls with Fingerly.load(apiKey:consent:) and identify(tag:), passing a granted consent state once you have consent. The result has the same fields as on the web. See iOS and Android, or React Native and Flutter. The mobile SDKs are coming soon and not yet available to install.
Replace server reads
import { load } from '@fingerly/node'
const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
const fired = (event: { triggers: Array<{ signal: string }> }, group: string) =>
event.triggers.some((trigger) => trigger.signal === group)
const event = await fingerly.events.get(requestId)
const { visitor_id, tag, occurred_at } = event
const isBot = fired(event, 'bot')
const usesVpn = fired(event, 'vpn')
| What you read today | Fingerly event |
|---|---|
| The visitor ID | visitor_id |
| The request ID | request_id |
| The linked ID and tag | tag |
| The timestamp, often in milliseconds | occurred_at (RFC 3339) |
| The IP address | ip_address |
| The IP location's country | country_code, country_name |
| The IP network | asn, asn_name, anonymity_network |
| Browser and device details | agent, agent_version, os, device_kind |
| A risk score | suspect_score, with suspect_level from your threshold |
| Each risk signal's result | A group in triggers, with its weight and confidence |
An event lists only the signal groups that fired. A group that is absent did not fire. Server libraries for Python, Go, Java, .NET, PHP, Ruby and Rust make the same calls.
Risk signals and signal groups
| Common risk signal | Fingerly group | Notes |
|---|---|---|
| Bad bot | bot | Automation, and clients that say they are bots. |
| Good bot | None | Read visitor_kind: search_crawler or ai_bot. |
| VPN | vpn | Weighed by how it was recognised or by confidence. See weighting modes. |
| Tor | tor | |
| Proxy | datacenter_proxy, residential_proxy | Two groups, weighted separately. |
| IP blocklist | ip_blocklist | Not active yet. |
| Incognito mode | incognito_mode | |
| Browser tampering | browser_tampering | |
| Virtual machine | virtual_machine | |
| Privacy-focused settings | privacy_settings | Also fingerprint_suppressed when too little can be read to identify anyone. |
| Developer tools | developer_tools | |
| High-activity device | high_activity | |
| Location spoofing | location_spoofing | |
| Remote control tools | remote_control | Browsers only. Not active yet. |
| Rooted device (Android) | rooted_device | |
| Emulator (Android) | android_emulator | The iOS Simulator is ios_simulator. |
| Cloned app (Android) | cloned_app | |
| Jailbroken device (iOS) | jailbroken_device | |
| Instrumentation framework | frida_detected | |
| Man-in-the-middle | mitm_attack | |
| Factory reset | device_farm | A signal that the device looks freshly reset or mass-provisioned, not a reset timestamp. |
| Velocity counts | None | Count per visitor_id in your own store. high_activity covers devices seen far more often than usual. |
| Raw device attributes | None | Fingerly does not expose what it collects from a device. |
Every group has a weight you can change, per organization or per key, and the score is their sum. See signals and risk weights.
Replace webhooks
| Common today | Fingerly | |
|---|---|---|
| What is sent | One request per identification, with its signals | identification.completed per identification; visitor.suspect with every signal when the level is high |
| Signature header | A provider-specific header | X-Fingerly-Signature: sha256=…, with X-Fingerly-Timestamp |
| Signed content | Often the body alone | The timestamp, a full stop, and the body |
| Replay protection | Often none in the signature | Reject timestamps more than five minutes old |
| Deduplication | Often by request ID | By the envelope id, also in X-Fingerly-Event-ID |
import { verifyWebhook } from '@fingerly/node'
const valid = verifyWebhook({
secret: process.env.FINGERLY_WEBHOOK_SECRET!,
payload: rawBody,
timestamp: req.get('x-fingerly-timestamp'),
signature: req.get('x-fingerly-signature'),
})
const { type, data } = JSON.parse(rawBody.toString())
If your consumer needs every signal for every identification, read the event by data.request_id when identification.completed arrives. See webhooks.
Proxy integrations
Many providers' proxy integrations forward both an agent download and its requests. Fingerly's SDK is part of your bundle, so a Fingerly proxy forwards one path, the random one the dashboard issues, to one API URL. Use the Cloudflare Worker, a server SDK's proxy, a ready-made template for Vercel, Netlify, AWS CloudFront, Google Cloud Run, nginx, Caddy, Traefik or a container, or your own proxy. A custom subdomain maps to a hosted subdomain, with the same weakness against DNS-level blockers. The new path is different from the old one, so both can run during the switch.
Identities do not carry over
- Store the Fingerly
visitor_idnext to your current provider's one on your accounts, orders and devices while both run. - Rules such as "known device for this account" start empty. Loosen new-device friction during the switch, or keep consulting the old IDs until customers have returned once.
- Counters per visitor, such as sign-ups per device, restart from zero. Keep old limits running on the old IDs until the window they cover has passed.
Other differences
- Environments. Keys belong to development, staging or production. Development and staging run the same detection and are free. See environments.
- Pricing. Prepaid, from $0.0025 per production identification, falling with monthly volume, with no plans or minimums. See billing.
- Retention. Events are readable for 30 days. Keep your own copy for longer. See data retention.
- Consent. Every SDK takes a
consentoption, defaults it topending, and collects nothing until it isgranted. If your current agent collects as soon as it is called, passgrantedor wire up your consent tool when you switch. See consent tools.