[{"data":1,"prerenderedAt":191},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdevice-fingerprinting-ios":3},{"page":4,"toc":167,"updated":190},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdevice-fingerprinting-ios","Add device fingerprinting to an iOS app (Swift)","iOS Device Fingerprinting in Swift: Step-by-Step Tutorial","Add device fingerprinting to an iOS app in Swift: install with Swift Package Manager, identify at sign-in, verify on your server, and set up App Attest.",[10,13,15,20,26,29,90,93,108,110,113,115,118,122,124,127,129,132,134,137,139,142,144],{"type":11,"text":12},"p","This tutorial adds **device fingerprinting** to a Swift iOS app with the Fingerly iOS SDK. The SDK reads raw device signals within a time budget and sends them with your public key. The server recognises the device and returns a visitor ID and a suspect score, and your backend decides what to do. There are no permission prompts and no third-party dependencies.",{"type":11,"text":14},"An iOS **device ID** from Fingerly is a visitor ID that comes from a random install ID the SDK keeps in the Keychain, never from the hardware. See [how apps are recognised](\u002Fdocs\u002Fvisitor-identification#in-apps).",{"type":16,"level":17,"text":18,"id":19},"heading",2,"Before you start","before-you-start",{"type":21,"items":22},"list",[23,24,25],"iOS 13 or newer, and Xcode 26.","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 `.granted`.",{"type":16,"level":17,"text":27,"id":28},"Add the SDK and verify the result","add-the-sdk-and-verify-the-result",{"type":30,"steps":31},"steps",[32,37,50,67,80,85],{"title":33,"blocks":34},"Create your keys",[35],{"type":11,"text":36},"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":38,"blocks":39},"Add the Swift package",[40,42],{"type":11,"text":41},"In Xcode, choose **File > Add Package Dependencies**, enter `https:\u002F\u002Fgithub.com\u002Ffingerly-io\u002Fsdk-ios` and add the **Fingerly** library to your app target. In a `Package.swift`, depend on the package and name its product:",{"type":43,"samples":44},"code",[45],{"label":46,"lang":47,"code":48,"check":49},"Package.swift","swift","dependencies: [\n    .package(url: \"https:\u002F\u002Fgithub.com\u002Ffingerly-io\u002Fsdk-ios\", from: \"0.2.0\"),\n],\ntargets: [\n    .target(\n        name: \"YourApp\",\n        dependencies: [.product(name: \"Fingerly\", package: \"sdk-ios\")]\n    ),\n]",false,{"title":51,"blocks":52},"Load the client once and identify at sign-in",[53,55,60,62],{"type":11,"text":54},"Load once and keep the client for the life of the app. `identify` runs on Swift concurrency. Pass a tag that names the action, so your server can check the identification belongs to it.",{"type":43,"samples":56},[57],{"label":58,"lang":47,"code":59,"check":49},"SignInViewModel.swift","import Fingerly\n\n\u002F\u002F consent: .granted only once you have consent (or another lawful basis)\nlet fingerly = try await Fingerly.load(apiKey: \"fly_pk_development_…\", consent: .granted)\n\nfunc signIn(email: String, password: String) async throws {\n    let result = try await fingerly.identify(tag: \"sign-in\")\n    try await api.signIn(email: email, password: password, requestId: result.requestId)\n}",{"type":11,"text":61},"If identification fails, carry on and let your server treat the missing request ID as missing evidence. Never block a customer only because the SDK could not reach Fingerly.",{"type":43,"samples":63},[64],{"label":65,"lang":47,"code":66,"check":49},"SignInView.swift","let requestId = try? await fingerly.identify(tag: \"sign-in\").requestId\ntry await api.signIn(email: email, password: password, requestId: requestId)",{"title":68,"blocks":69},"Verify on your server",[70,72,78],{"type":11,"text":71},"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":43,"samples":73},[74],{"label":75,"lang":76,"code":77,"check":49},"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":79},"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":81,"blocks":82},"Gate collection behind consent",[83],{"type":11,"text":84},"`consent` defaults to `.pending`, which collects nothing. Load with `.pending` and call `setConsent(.granted)` from your consent tool, or pass `.granted` as above once you have consent or another lawful basis. See [consent tools](\u002Fdocs\u002Fconsent-tools#mobile-apps).",{"title":86,"blocks":87},"Run it on a device",[88],{"type":11,"text":89},"Run the app, sign in, then open **Identification > Events** in the dashboard to see the visitor, score and signals. On the iOS Simulator the event records the `ios_simulator` signal, which is expected there, and App Attest needs a real device.",{"type":16,"level":17,"text":91,"id":92},"What the visitor ID survives","what-the-visitor-id-survives",{"type":94,"columns":95,"rows":98},"table",[96,97],"Event","Effect on the visitor ID",[99,102,105],[100,101],"The app is closed and reopened, or updated","Unchanged.",[103,104],"The app is deleted and installed again on the same device","Unchanged. The Keychain record survives deleting the app.",[106,107],"`Fingerly.deleteStoredIdentity()` is called","The next identification made with consent gets a new install ID and a new visitor ID.",{"type":11,"text":109},"The record is never in a backup. Call `deleteStoredIdentity()` for erasure requests and account deletion. See [account deletion and erasure](\u002Fdocs\u002Fapp-store-privacy#account-deletion-and-erasure).",{"type":16,"level":17,"text":111,"id":112},"Prove the report comes from your app","prove-the-report-comes-from-your-app",{"type":11,"text":114},"A modified app can forge everything else in a report, so on iOS set up App Attest. It needs the App Attest capability on your target and your [Apple app ID on the public key](\u002Fdocs\u002Fapi-keys#native-apps), and works on iOS 14 or newer on a real device. See [App Attest in the iOS SDK](\u002Fdocs\u002Fsdks\u002Fios#app-attest) and [mobile app attestation](\u002Fdocs\u002Fmobile-attestation).",{"type":16,"level":17,"text":116,"id":117},"Common questions","common-questions",{"type":16,"level":119,"text":120,"id":121},3,"How do I identify an iOS device without a permission prompt?","how-do-i-identify-an-ios-device-without-a-permission-prompt",{"type":11,"text":123},"Use the iOS SDK. It never shows a permission prompt, and it does not use App Tracking Transparency because it does not track. The device is recognised by a random install ID the SDK keeps in the Keychain.",{"type":16,"level":119,"text":125,"id":126},"Does Fingerly read the identifierForVendor or the advertising identifier?","does-fingerly-read-the-identifierforvendor-or-the-advertising-identifier",{"type":11,"text":128},"No. Native identity is the random install ID alone. Device characteristics feed the integrity checks and the score, never the identity. See [App Store and Google Play privacy](\u002Fdocs\u002Fapp-store-privacy#apple).",{"type":16,"level":119,"text":130,"id":131},"Does the visitor ID survive an app reinstall on iOS?","does-the-visitor-id-survive-an-app-reinstall-on-ios",{"type":11,"text":133},"Yes, on the same device, because the install record lives in the Keychain, which outlives the app. `deleteStoredIdentity()` is the way to remove it.",{"type":16,"level":119,"text":135,"id":136},"Can I use the iOS SDK from Objective-C or inside a web view?","can-i-use-the-ios-sdk-from-objective-c-or-inside-a-web-view",{"type":11,"text":138},"From Objective-C, yes, through `FingerlyBridge`. Do not use the browser SDK inside a `WKWebView`: it collects nothing there and the answer has `identifiable: false`. Use this SDK in the app instead.",{"type":16,"level":119,"text":140,"id":141},"Is it safe to act on the result in the app?","is-it-safe-to-act-on-the-result-in-the-app",{"type":11,"text":143},"Use `suspectLevel` only to adapt the interface, for example to add a step. The score reaches the app as a convenience, and a modified app can ignore it. Decide on your backend, from the stored event.",{"type":145,"columns":17,"cards":146},"cards",[147,152,157,162],{"title":148,"text":149,"href":150,"logo":151},"iOS SDK","Configuration, result, App Attest and errors.","\u002Fdocs\u002Fsdks\u002Fios","apple",{"title":153,"text":154,"href":155,"icon":156},"Mobile app attestation","Proof a report comes from your genuine app.","\u002Fdocs\u002Fmobile-attestation","shield",{"title":158,"text":159,"href":160,"icon":161},"Emulators, root and jailbreak","What the SDKs report about modified devices.","\u002Fdocs\u002Fdetect-emulators-root-jailbreak","mobile",{"title":163,"text":164,"href":165,"icon":166},"App Store privacy","The App Privacy answers for the SDK.","\u002Fdocs\u002Fapp-store-privacy","lock",[168,169,170,172,174,176,178,180,182,183,184,185,186,187,188,189],{"id":19,"text":18,"level":17},{"id":28,"text":27,"level":17},{"id":171,"text":33,"level":119},"step-create-your-keys",{"id":173,"text":38,"level":119},"step-add-the-swift-package",{"id":175,"text":51,"level":119},"step-load-the-client-once-and-identify-at-sign-in",{"id":177,"text":68,"level":119},"step-verify-on-your-server",{"id":179,"text":81,"level":119},"step-gate-collection-behind-consent",{"id":181,"text":86,"level":119},"step-run-it-on-a-device",{"id":92,"text":91,"level":17},{"id":112,"text":111,"level":17},{"id":117,"text":116,"level":17},{"id":121,"text":120,"level":119},{"id":126,"text":125,"level":119},{"id":131,"text":130,"level":119},{"id":136,"text":135,"level":119},{"id":141,"text":140,"level":119},"2026-11-12T14:07:07.000Z",1794493376740]