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

Last updated: 2026-11-12

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](https://docs.fingerly.io/docs/visitor-identification#in-apps).

## 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

### 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](https://docs.fingerly.io/docs/api-keys#native-apps).

### 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:

```swift 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")]
    ),
]
```

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

```swift 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.

```swift SignInView.swift
let requestId = try? await fingerly.identify(tag: "sign-in").requestId
try await api.signIn(email: email, password: password, requestId: requestId)
```

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

```ts 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](https://docs.fingerly.io/docs/server-side-verification) explains each check, and the [use-case recipes](https://docs.fingerly.io/docs/use-cases) include a helper that does all of them.

### Step 5: Gate collection behind consent

`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](https://docs.fingerly.io/docs/consent-tools#mobile-apps).

### 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

| Event | Effect on the visitor ID |
| --- | --- |
| The app is closed and reopened, or updated | Unchanged. |
| The app is deleted and installed again on the same device | Unchanged. The Keychain record survives deleting the app. |
| `Fingerly.deleteStoredIdentity()` is called | The 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](https://docs.fingerly.io/docs/app-store-privacy#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](https://docs.fingerly.io/docs/api-keys#native-apps), and works on iOS 14 or newer on a real device. See [App Attest in the iOS SDK](https://docs.fingerly.io/docs/sdks/ios#app-attest) and [mobile app attestation](https://docs.fingerly.io/docs/mobile-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](https://docs.fingerly.io/docs/app-store-privacy#apple).

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

- [iOS SDK](https://docs.fingerly.io/docs/sdks/ios): Configuration, result, App Attest and errors.
- [Mobile app attestation](https://docs.fingerly.io/docs/mobile-attestation): Proof a report comes from your genuine app.
- [Emulators, root and jailbreak](https://docs.fingerly.io/docs/detect-emulators-root-jailbreak): What the SDKs report about modified devices.
- [App Store privacy](https://docs.fingerly.io/docs/app-store-privacy): The App Privacy answers for the SDK.
