Guides

Add device fingerprinting to an iOS app (Swift)

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.

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.

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.

Before you start

  • 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.

Add the SDK and verify the result

  1. Step 1: Create your keys

    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.

  2. Step 2: Add the Swift package

    In Xcode, choose File > Add Package Dependencies, enter https://github.com/fingerly-io/sdk-ios and add the Fingerly library to your app target. In a Package.swift, depend on the package and name its product:

    Package.swift
    dependencies: [
        .package(url: "https://github.com/fingerly-io/sdk-ios", from: "0.2.0"),
    ],
    targets: [
        .target(
            name: "YourApp",
            dependencies: [.product(name: "Fingerly", package: "sdk-ios")]
        ),
    ]
    
  3. Step 3: Load the client once and identify at sign-in

    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.

    SignInViewModel.swift
    import Fingerly
    
    // consent: .granted only once you have consent (or another lawful basis)
    let fingerly = try await Fingerly.load(apiKey: "fly_pk_development_…", consent: .granted)
    
    func signIn(email: String, password: String) async throws {
        let result = try await fingerly.identify(tag: "sign-in")
        try await api.signIn(email: email, password: password, requestId: result.requestId)
    }
    

    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.

    SignInView.swift
    let requestId = try? await fingerly.identify(tag: "sign-in").requestId
    try await api.signIn(email: email, password: password, requestId: requestId)
    
  4. Step 4: Verify on your server

    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.

    Sign-in handler (Node.js)
    import { FingerlyAPIError, load } from '@fingerly/node'
    
    const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
    
    export async function afterPasswordCheck(requestId: unknown) {
      if (typeof requestId !== 'string' || requestId === '') return 'second-factor' // missing evidence
    
      try {
        const event = await fingerly.events.get(requestId)
        if (event.tag !== 'sign-in') return 'second-factor'
        if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor'
        return event.suspect_level === 'high' ? 'second-factor' : 'allow'
      } catch (error) {
        if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor'
        throw error // still pending: answer with a retry, never a refusal
      }
    }
    

    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 explains each check, and the use-case recipes include a helper that does all of them.

  5. 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.

  6. Step 6: Run it on a device

    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.

What the visitor ID survives

EventEffect on the visitor ID
The app is closed and reopened, or updatedUnchanged.
The app is deleted and installed again on the same deviceUnchanged. The Keychain record survives deleting the app.
Fingerly.deleteStoredIdentity() is calledThe next identification made with consent gets a new install ID and a new visitor ID.

The record is never in a backup. Call deleteStoredIdentity() for erasure requests and account deletion. See account deletion and erasure.

Prove the report comes from your app

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, and works on iOS 14 or newer on a real device. See App Attest in the iOS SDK and mobile app attestation.

Common questions

How do I identify an iOS device without a permission prompt?

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.

Does Fingerly read the identifierForVendor or the advertising identifier?

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.

Does the visitor ID survive an app reinstall on iOS?

Yes, on the same device, because the install record lives in the Keychain, which outlives the app. deleteStoredIdentity() is the way to remove it.

Can I use the iOS SDK from Objective-C or inside a web view?

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.

Is it safe to act on the result in the app?

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.