[{"data":1,"prerenderedAt":423},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fmigrate":3},{"page":4,"toc":398,"updated":422},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fmigrate","Migrate from another provider","Migrate from Another Device Identification Provider to Fingerly","Move an existing device identification integration to Fingerly: map the client call, server reads, signals and webhooks, then cut over without a gap.",[10,13,17,22,61,64,92,95,98,105,109,138,141,178,180,183,189,191,194,196,199,204,237,239,242,325,327,330,354,358,360,363,365,368,374,377,383,386],{"type":11,"text":12},"p","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.",{"type":14,"tone":15,"text":16},"callout","note","Field and option names differ from one provider to the next. The rows below describe what each part does, so match them to the names in your current integration.",{"type":18,"level":19,"text":20,"id":21},"heading",2,"At a glance","at-a-glance",{"type":23,"columns":24,"rows":27},"table",[25,26],"What you use today","In Fingerly",[28,31,34,37,40,43,46,49,52,55,58],[29,30],"Load the client agent with a public key","`load({ apiKey })`",[32,33],"Identify the visitor at a moment that matters","`fingerly.identify()`",[35,36],"A linked ID and a tag or custom data","One `tag` string",[38,39],"A visitor ID","`visitorId`, scoped to your organization",[41,42],"A match confidence, often 0 to 1","`visitorConfidence`, 0 to 100",[44,45],"Risk signals and a risk score","Signal groups, `suspect_score` and `suspect_level`",[47,48],"Read one result on your server","[`GET \u002Fapi\u002Fv1\u002Fevents\u002F{request_id}`](\u002Freference\u002Fget-event)",[50,51],"A visitor's history","[`GET \u002Fapi\u002Fv1\u002Fevents?visitor=…`](\u002Freference\u002Flist-events), 30 days",[53,54],"A webhook per identification","[`identification.completed`](\u002Freference\u002Fwebhooks\u002Fidentification-completed), and [`visitor.suspect`](\u002Freference\u002Fwebhooks\u002Fvisitor-suspect) with signals",[56,57],"A region option","None. One API, one origin",[59,60],"A custom subdomain or a cloud proxy","[Proxy integrations](\u002Fdocs\u002Fproxy-integrations), or a [hosted subdomain](\u002Fdocs\u002Fhosted-subdomain) as a fallback",{"type":18,"level":19,"text":62,"id":63},"Plan the switch","plan-the-switch",{"type":65,"steps":66},"steps",[67,72,77,82,87],{"title":68,"blocks":69},"Create Fingerly keys",[70],{"type":11,"text":71},"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](\u002Fdocs\u002Fapi-keys).",{"title":73,"blocks":74},"Run both agents side by side",[75],{"type":11,"text":76},"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.",{"title":78,"blocks":79},"Move server checks",[80],{"type":11,"text":81},"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.",{"title":83,"blocks":84},"Move webhooks",[85],{"type":11,"text":86},"Register Fingerly endpoints, verify their signatures, and point your consumers at them.",{"title":88,"blocks":89},"Remove the old agent",[90],{"type":11,"text":91},"Delete the old agent, its proxy routes and its keys.",{"type":14,"tone":93,"text":94},"warning","Visitor IDs do not carry over. Fingerly issues its own, so a device you knew under your current provider's visitor ID starts as a new visitor. Store both IDs while you run side by side, and expect more \"new device\" decisions for a while. See [identities do not carry over](#identities-do-not-carry-over).",{"type":18,"level":19,"text":96,"id":97},"Replace the JavaScript agent","replace-the-javascript-agent",{"type":99,"samples":100},"code",[101],{"label":102,"lang":103,"code":104},"fingerly.ts","ts","import { load } from '@fingerly\u002Fweb-js'\n\nconst fingerly = await load({\n  apiKey: 'fly_pk_production_…',\n  endpoints: '\u002Fk7x2qm4a',\n  fallbackToDefaultEndpoint: true,\n  consent: 'granted', \u002F\u002F only once you have consent (or another lawful basis)\n})\n\nconst { requestId, visitorId } = await fingerly.identify({ tag: 'login' })",{"type":18,"level":106,"text":107,"id":108},3,"Load options","load-options",{"type":23,"columns":110,"rows":114},[111,112,113],"What you set today","Fingerly","Notes",[115,119,123,127,131,134],[116,117,118],"A public API key","`apiKey`","A Fingerly public key, `fly_pk_…`.",[120,121,122],"A region","None","Fingerly is one API at one origin. There is nothing to choose.",[124,125,126],"Custom endpoints","`endpoints`","The path the dashboard issued for a [proxy](\u002Fdocs\u002Fproxy-integrations), such as `\u002Fk7x2qm4a`, or a URL. Posted to exactly as given, with nothing appended.",[128,129,130],"A fallback to the provider's own endpoint","`fallbackToDefaultEndpoint: true`","Try the Fingerly API after your own endpoints fail.",[132,121,133],"A script URL to proxy","The SDK is bundled into your application. Nothing is downloaded at runtime, so there is no script to proxy.",[135,136,137],"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](\u002Fdocs\u002Fprivacy-and-consent#consent).",{"type":18,"level":106,"text":139,"id":140},"Identify options and result","identify-options-and-result",{"type":23,"columns":142,"rows":143},[25,112,113],[144,148,152,155,159,163,167,171,175],[145,146,147],"A linked ID","Part of `tag`","Fingerly has one reference field. Put the account or order in it, such as `login` or `checkout:8412`.",[149,150,151],"A tag of any JSON","`tag` (a string)","Encode what you need as a string. Your server compares it before trusting the result.",[153,121,154],"An extended result","Network and device details are on the stored event your server reads.",[156,157,158],"A timeout","`signal`","Pass an `AbortSignal`. Collection is already capped at 300 ms, and each attempt at 5 seconds.",[160,161,162],"The request ID","`requestId`","Send it to your server with the action.",[164,165,166],"The visitor ID","`visitorId`","Twenty letters and digits, scoped to your organization.",[168,169,170],"A confidence from 0 to 1","`visitorConfidence`","0 to 100. See [visitor confidence](\u002Fdocs\u002Fvisitor-identification#visitor-confidence).",[172,173,174],"Whether the visitor was found before","`visitorIsNew`","Usually the opposite sense.",[176,121,177],"An encrypted or sealed result","Read the event on your server with a secret key instead.",{"type":11,"text":179},"Fingerly also returns the score, the level and the signals in the same response, and local [verdicts](\u002Fdocs\u002Fclient-verdicts) computed in the browser. Use them to adapt the interface; decide on your server.",{"type":18,"level":19,"text":181,"id":182},"Replace a React library","replace-a-react-library",{"type":99,"samples":184},[185],{"label":186,"lang":187,"code":188},"App.tsx","tsx","import { FingerlyProvider, useIdentify } from '@fingerly\u002Freact'\n\n\u003CFingerlyProvider apiKey=\"fly_pk_production_…\" consent=\"granted\">\n  \u003CApp \u002F>\n\u003C\u002FFingerlyProvider>\n\nconst { identify, isLoading } = useIdentify({ tag: 'login' })\nconst { requestId } = await identify({ force: true })",{"type":11,"text":190},"`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](\u002Fdocs\u002Fsdks).",{"type":18,"level":106,"text":192,"id":193},"Mobile","mobile",{"type":11,"text":195},"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](\u002Fdocs\u002Fsdks\u002Fios) and [Android](\u002Fdocs\u002Fsdks\u002Fandroid), or [React Native](\u002Fdocs\u002Fsdks\u002Freact-native) and [Flutter](\u002Fdocs\u002Fsdks\u002Fflutter). The mobile SDKs are coming soon and not yet available to install.",{"type":18,"level":19,"text":197,"id":198},"Replace server reads","replace-server-reads",{"type":99,"samples":200},[201],{"label":202,"lang":103,"code":203},"verify.ts","import { load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\nconst fired = (event: { triggers: Array\u003C{ signal: string }> }, group: string) =>\n  event.triggers.some((trigger) => trigger.signal === group)\n\nconst event = await fingerly.events.get(requestId)\nconst { visitor_id, tag, occurred_at } = event\nconst isBot = fired(event, 'bot')\nconst usesVpn = fired(event, 'vpn')",{"type":23,"columns":205,"rows":208},[206,207],"What you read today","Fingerly event",[209,211,213,216,219,222,225,228,231,234],[164,210],"`visitor_id`",[160,212],"`request_id`",[214,215],"The linked ID and tag","`tag`",[217,218],"The timestamp, often in milliseconds","`occurred_at` (RFC 3339)",[220,221],"The IP address","`ip_address`",[223,224],"The IP location's country","`country_code`, `country_name`",[226,227],"The IP network","`asn`, `asn_name`, `anonymity_network`",[229,230],"Browser and device details","`agent`, `agent_version`, `os`, `device_kind`",[232,233],"A risk score","`suspect_score`, with `suspect_level` from your threshold",[235,236],"Each risk signal's result","A group in `triggers`, with its `weight` and `confidence`",{"type":11,"text":238},"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](\u002Fdocs\u002Fsdks#server) make the same calls.",{"type":18,"level":19,"text":240,"id":241},"Risk signals and signal groups","risk-signals-and-signal-groups",{"type":23,"columns":243,"rows":246},[244,245,113],"Common risk signal","Fingerly group",[247,251,254,258,262,266,270,273,276,279,283,286,289,292,296,299,303,306,309,312,315,319,322],[248,249,250],"Bad bot","`bot`","Automation, and clients that say they are bots.",[252,121,253],"Good bot","Read `visitor_kind`: `search_crawler` or `ai_bot`.",[255,256,257],"VPN","`vpn`","Weighed by how it was recognised or by confidence. See [weighting modes](\u002Fdocs\u002Frisk-weights#weighting-modes).",[259,260,261],"Tor","`tor`","",[263,264,265],"Proxy","`datacenter_proxy`, `residential_proxy`","Two groups, weighted separately.",[267,268,269],"IP blocklist","`ip_blocklist`","Not active yet.",[271,272,261],"Incognito mode","`incognito_mode`",[274,275,261],"Browser tampering","`browser_tampering`",[277,278,261],"Virtual machine","`virtual_machine`",[280,281,282],"Privacy-focused settings","`privacy_settings`","Also `fingerprint_suppressed` when too little can be read to identify anyone.",[284,285,261],"Developer tools","`developer_tools`",[287,288,261],"High-activity device","`high_activity`",[290,291,261],"Location spoofing","`location_spoofing`",[293,294,295],"Remote control tools","`remote_control`","Browsers only. Not active yet.",[297,298,261],"Rooted device (Android)","`rooted_device`",[300,301,302],"Emulator (Android)","`android_emulator`","The iOS Simulator is `ios_simulator`.",[304,305,261],"Cloned app (Android)","`cloned_app`",[307,308,261],"Jailbroken device (iOS)","`jailbroken_device`",[310,311,261],"Instrumentation framework","`frida_detected`",[313,314,261],"Man-in-the-middle","`mitm_attack`",[316,317,318],"Factory reset","`device_farm`","A signal that the device looks freshly reset or mass-provisioned, not a reset timestamp.",[320,121,321],"Velocity counts","Count per `visitor_id` in your own store. `high_activity` covers devices seen far more often than usual.",[323,121,324],"Raw device attributes","Fingerly does not expose what it collects from a device.",{"type":11,"text":326},"Every group has a weight you can change, per organization or per key, and the score is their sum. See [signals](\u002Fdocs\u002Fsignals) and [risk weights](\u002Fdocs\u002Frisk-weights).",{"type":18,"level":19,"text":328,"id":329},"Replace webhooks","replace-webhooks",{"type":23,"columns":331,"rows":333},[261,332,112],"Common today",[334,338,342,346,350],[335,336,337],"What is sent","One request per identification, with its signals","`identification.completed` per identification; `visitor.suspect` with every signal when the level is `high`",[339,340,341],"Signature header","A provider-specific header","`X-Fingerly-Signature: sha256=…`, with `X-Fingerly-Timestamp`",[343,344,345],"Signed content","Often the body alone","The timestamp, a full stop, and the body",[347,348,349],"Replay protection","Often none in the signature","Reject timestamps more than five minutes old",[351,352,353],"Deduplication","Often by request ID","By the envelope `id`, also in `X-Fingerly-Event-ID`",{"type":99,"samples":355},[356],{"label":112,"lang":103,"code":357},"import { verifyWebhook } from '@fingerly\u002Fnode'\n\nconst valid = verifyWebhook({\n  secret: process.env.FINGERLY_WEBHOOK_SECRET!,\n  payload: rawBody,\n  timestamp: req.get('x-fingerly-timestamp'),\n  signature: req.get('x-fingerly-signature'),\n})\nconst { type, data } = JSON.parse(rawBody.toString())",{"type":11,"text":359},"If your consumer needs every signal for every identification, read the event by `data.request_id` when `identification.completed` arrives. See [webhooks](\u002Fdocs\u002Fwebhooks).",{"type":18,"level":19,"text":361,"id":362},"Proxy integrations","proxy-integrations",{"type":11,"text":364},"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](\u002Fdocs\u002Fsdks\u002Fcloudflare-worker), a [server SDK's proxy](\u002Fdocs\u002Fproxy-integrations#options), a ready-made template for [Vercel](\u002Fdocs\u002Fproxies\u002Fvercel), [Netlify](\u002Fdocs\u002Fproxies\u002Fnetlify), [AWS CloudFront](\u002Fdocs\u002Fproxies\u002Faws-cloudfront), [Google Cloud Run](\u002Fdocs\u002Fproxies\u002Fgoogle-cloud-run), [nginx](\u002Fdocs\u002Fproxies\u002Fnginx), [Caddy](\u002Fdocs\u002Fproxies\u002Fcaddy), [Traefik](\u002Fdocs\u002Fproxies\u002Ftraefik) or a [container](\u002Fdocs\u002Fproxies\u002Fcontainer), or [your own proxy](\u002Fdocs\u002Fproxy-integrations#build-your-own-proxy). A custom subdomain maps to a [hosted subdomain](\u002Fdocs\u002Fhosted-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.",{"type":18,"level":19,"text":366,"id":367},"Identities do not carry over","identities-do-not-carry-over",{"type":369,"items":370},"list",[371,372,373],"Store the Fingerly `visitor_id` next 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.",{"type":18,"level":19,"text":375,"id":376},"Other differences","other-differences",{"type":369,"items":378},[379,380,381,382],"**Environments.** Keys belong to development, staging or production. Development and staging run the same detection and are free. See [environments](\u002Fdocs\u002Fapi-keys#environments).","**Pricing.** Prepaid, from $0.0025 per production identification, falling with monthly volume, with no plans or minimums. See [billing](\u002Fdocs\u002Fbilling).","**Retention.** Events are readable for 30 days. Keep your own copy for longer. See [data retention](\u002Fdocs\u002Fdata-retention).","**Consent.** Every SDK takes a `consent` option, defaults it to `pending`, and collects nothing until it is `granted`. If your current agent collects as soon as it is called, pass `granted` or wire up your consent tool when you switch. See [consent tools](\u002Fdocs\u002Fconsent-tools).",{"type":14,"tone":384,"text":385},"tip","Stuck on something this page does not map? Email [support@fingerly.io](mailto:support@fingerly.io) with the feature you use today.",{"type":387,"columns":19,"cards":388},"cards",[389,394],{"title":390,"text":391,"href":392,"icon":393},"Quick start","The Fingerly flow end to end.","\u002Fdocs\u002Fquick-start","bolt",{"title":395,"text":396,"href":397,"icon":369},"Use cases","Recipes for the policies you are moving.","\u002Fdocs\u002Fuse-cases",[399,400,401,403,405,407,409,411,412,413,414,415,416,417,418,419,420,421],{"id":21,"text":20,"level":19},{"id":63,"text":62,"level":19},{"id":402,"text":68,"level":106},"step-create-fingerly-keys",{"id":404,"text":73,"level":106},"step-run-both-agents-side-by-side",{"id":406,"text":78,"level":106},"step-move-server-checks",{"id":408,"text":83,"level":106},"step-move-webhooks",{"id":410,"text":88,"level":106},"step-remove-the-old-agent",{"id":97,"text":96,"level":19},{"id":108,"text":107,"level":106},{"id":140,"text":139,"level":106},{"id":182,"text":181,"level":19},{"id":193,"text":192,"level":106},{"id":198,"text":197,"level":19},{"id":241,"text":240,"level":19},{"id":329,"text":328,"level":19},{"id":362,"text":361,"level":19},{"id":367,"text":366,"level":19},{"id":376,"text":375,"level":19},"2026-11-04T11:02:39.000Z",1793804718316]