[{"data":1,"prerenderedAt":178},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdevice-fingerprinting-flutter":3},{"page":4,"toc":155,"updated":177},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdevice-fingerprinting-flutter","Add device fingerprinting to a Flutter app","Flutter Device Fingerprinting: Identify Devices in Dart","Add device fingerprinting to a Flutter app with a typed Dart API over the native iOS and Android SDKs, then verify the result on your server.",[10,13,18,24,27,87,90,92,97,99,103,106,110,112,115,117,120,122,125,127,130,132],{"type":11,"text":12},"p","This tutorial adds **device fingerprinting** to a Flutter app. `fingerly_flutter` is a plugin over the native iOS and Android SDKs: collection happens in native code, and Dart gets typed results. The server returns a visitor ID and a suspect score, and your backend decides what to do.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Before you start","before-you-start",{"type":19,"items":20},"list",[21,22,23],"Flutter 3.10 or newer with Dart 3, and iOS 13 and Android 5.0 (API 21) or newer.","A public key for the development environment, and a secret key for your server.","A decision about consent: the SDK collects nothing until `consent` is `ConsentState.granted`.",{"type":14,"level":15,"text":25,"id":26},"Add the SDK and verify the result","add-the-sdk-and-verify-the-result",{"type":28,"steps":29},"steps",[30,35,47,59,72,82],{"title":31,"blocks":32},"Create your keys",[33],{"type":11,"text":34},"In the Fingerly dashboard, open **Integration > SDK keys** and create a **public** key in the **development** environment, and a **secret** key in the same environment for your server. Development keys run the same detection as production and cost nothing. A production key must also name your app, an Android package or an Apple app ID, as in [API keys and environments](\u002Fdocs\u002Fapi-keys#native-apps).",{"title":36,"blocks":37},"Add the plugin",[38,45],{"type":39,"samples":40},"code",[41],{"label":42,"lang":43,"code":44},"Terminal","bash","flutter pub add fingerly_flutter",{"type":11,"text":46},"On iOS, the plugin fetches the native framework on the first build and checks it against a pinned checksum, through Swift Package Manager or CocoaPods, whichever your app uses. On Android, it adds Fingerly's Maven repository to the build itself.",{"title":48,"blocks":49},"Load the client and identify at sign-in",[50,57],{"type":39,"samples":51},[52],{"label":53,"lang":54,"code":55,"check":56},"lib\u002Ffingerly.dart","dart","import 'package:fingerly_flutter\u002Ffingerly_flutter.dart';\n\n\u002F\u002F consent: granted only once you have consent (or another lawful basis)\nfinal fingerly = await Fingerly.load(apiKey: 'fly_pk_development_…', consent: ConsentState.granted);\n\nFuture\u003Cvoid> signIn(String email, String password) async {\n  final result = await fingerly.identify(tag: 'sign-in');\n  await api.signIn(email, password, requestId: result.requestId);\n}",false,{"type":11,"text":58},"`suspectScore` and `suspectLevel` are `null` when absent, and `visitorIsNew`, `visitorConfidence`, `identifiable` and `duplicate` are nullable too. With `submit: false`, only `report` is filled in.",{"title":60,"blocks":61},"Verify on your server",[62,64,70],{"type":11,"text":63},"A phone is a device you do not control, so the decision is never made in the app. Send the `requestId` to your backend with the action, read the stored event with your secret key, and check it belongs to this action and is recent.",{"type":39,"samples":65},[66],{"label":67,"lang":68,"code":69,"check":56},"Sign-in handler (Node.js)","ts","import { FingerlyAPIError, load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\n\nexport async function afterPasswordCheck(requestId: unknown) {\n  if (typeof requestId !== 'string' || requestId === '') return 'second-factor' \u002F\u002F missing evidence\n\n  try {\n    const event = await fingerly.events.get(requestId)\n    if (event.tag !== 'sign-in') return 'second-factor'\n    if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor'\n    return event.suspect_level === 'high' ? 'second-factor' : 'allow'\n  } catch (error) {\n    if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor'\n    throw error \u002F\u002F still pending: answer with a retry, never a refusal\n  }\n}",{"type":11,"text":71},"Also check the request ID has not been used before, and answer a pending event with a retry rather than a refusal. [Server-side verification](\u002Fdocs\u002Fserver-side-verification) explains each check, and the [use-case recipes](\u002Fdocs\u002Fuse-cases) include a helper that does all of them.",{"title":73,"blocks":74},"Handle errors",[75,80],{"type":39,"samples":76},[77],{"label":78,"lang":54,"code":79,"check":56},"errors.dart","try {\n  await fingerly.identify();\n} on FingerlyException catch (error) {\n  if (error.status == 401) { \u002F* the key is wrong *\u002F }\n  if (error.status == 402) { \u002F* the organization is not accepting traffic *\u002F }\n}",{"type":11,"text":81},"Until consent is `granted`, `identify` and `collect` throw `FingerlyConsentException`. If identification fails for any reason, let the user continue and send no request ID.",{"title":83,"blocks":84},"Run it on both platforms",[85],{"type":11,"text":86},"Run the app on an iOS and an Android target, sign in, then open **Identification > Events** in the dashboard to see the visitor, score and signals for each.",{"type":14,"level":15,"text":88,"id":89},"Consent and erasure","consent-and-erasure",{"type":11,"text":91},"Load with the default `ConsentState.pending` and call `setConsent` from your consent tool, or pass `granted` once you have consent or another lawful basis. The consent state is shared by the whole app, so pass it on every `load`.",{"type":39,"samples":93},[94],{"label":95,"lang":54,"code":96,"check":56},"lib\u002Fdelete_account.dart","await Fingerly.deleteStoredIdentity();",{"type":11,"text":98},"This deletes the install record on the device, for erasure requests and account deletion. See [account deletion and erasure](\u002Fdocs\u002Fapp-store-privacy#account-deletion-and-erasure).",{"type":100,"tone":101,"text":102},"callout","tip","On iOS the identity survives the app being deleted, and on Android it does not. A reinstalled Android app gets a new visitor ID. See [visitor identification](\u002Fdocs\u002Fvisitor-identification#in-apps).",{"type":14,"level":15,"text":104,"id":105},"Common questions","common-questions",{"type":14,"level":107,"text":108,"id":109},3,"Does the plugin fingerprint in Dart?","does-the-plugin-fingerprint-in-dart",{"type":11,"text":111},"No. Collection happens in the native iOS and Android SDKs, and Dart receives typed results. The same visitor ID rules apply as in a native app.",{"type":14,"level":107,"text":113,"id":114},"Which platforms does the plugin support?","which-platforms-does-the-plugin-support",{"type":11,"text":116},"iOS 13 and newer and Android 5.0 (API 21) and newer, with Flutter 3.10 or newer. Never use the browser SDK inside an app's web view: it collects nothing there and the answer has `identifiable: false`.",{"type":14,"level":107,"text":118,"id":119},"What is the identity on a Flutter device?","what-is-the-identity-on-a-flutter-device",{"type":11,"text":121},"A random install ID the native SDK keeps, never a hardware identifier. It lives in the Keychain on iOS and in a file excluded from backups on Android.",{"type":14,"level":107,"text":123,"id":124},"How do I read the native SDK version?","how-do-i-read-the-native-sdk-version",{"type":11,"text":126},"Call `Fingerly.nativeVersion()`, which returns a `Future\u003CString>`.",{"type":14,"level":107,"text":128,"id":129},"Can I decide in Dart whether to allow the action?","can-i-decide-in-dart-whether-to-allow-the-action",{"type":11,"text":131},"You can adapt the interface, for example to add a step when `suspectLevel` is `high`. The decision itself belongs on your backend, from the stored event, because an app can be modified.",{"type":133,"columns":15,"cards":134},"cards",[135,140,145,150],{"title":136,"text":137,"href":138,"logo":139},"Flutter SDK","Every member, field and error.","\u002Fdocs\u002Fsdks\u002Fflutter","flutter",{"title":141,"text":142,"href":143,"logo":144},"React Native tutorial","The same shape in TypeScript.","\u002Fdocs\u002Fdevice-fingerprinting-react-native","react",{"title":146,"text":147,"href":148,"icon":149},"Mobile app attestation","Proof a report comes from your genuine app.","\u002Fdocs\u002Fmobile-attestation","shield",{"title":151,"text":152,"href":153,"icon":154},"Server-side verification","The four checks before you trust a result.","\u002Fdocs\u002Fserver-side-verification","server",[156,157,158,160,162,164,166,168,170,171,172,173,174,175,176],{"id":17,"text":16,"level":15},{"id":26,"text":25,"level":15},{"id":159,"text":31,"level":107},"step-create-your-keys",{"id":161,"text":36,"level":107},"step-add-the-plugin",{"id":163,"text":48,"level":107},"step-load-the-client-and-identify-at-sign-in",{"id":165,"text":60,"level":107},"step-verify-on-your-server",{"id":167,"text":73,"level":107},"step-handle-errors",{"id":169,"text":83,"level":107},"step-run-it-on-both-platforms",{"id":89,"text":88,"level":15},{"id":105,"text":104,"level":15},{"id":109,"text":108,"level":107},{"id":114,"text":113,"level":107},{"id":119,"text":118,"level":107},{"id":124,"text":123,"level":107},{"id":129,"text":128,"level":107},"2026-11-12T11:59:51.000Z",1794493376756]