# iOS

> Identify iPhones and iPads with the native Swift SDK. No dependencies, no permission prompts, and jailbreak, simulator and tampering verdicts on the device.

Last updated: 2026-09-17

The iOS SDK is written in Swift with no third-party dependencies. It collects device signals within a time budget, submits them with your public key, and returns the server's answer with local verdicts. It never shows a permission prompt.

## Requirements

- iOS 13 or newer (also tvOS 13 and Mac Catalyst 13).
- Xcode 16 or newer.
- A [public key](https://docs.fingerly.io/docs/api-keys).

## Install

```swift Swift Package Manager
.package(url: "https://github.com/fingerly-io/sdk-ios.git", from: "0.1.0")
```

```ruby CocoaPods
pod 'Fingerly', '~> 0.1'
```

In Xcode, add the package from **File > Add Package Dependencies** and link the `Fingerly` product to your app target.

## Identify a device

```swift SignInViewModel.swift
import Fingerly

let fingerly = try await Fingerly.load(apiKey: "fly_pk_us_production_…")

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)
}
```

Load once and keep the client for the life of the app. `identify` runs on Swift concurrency and never touches the main thread.

Send `requestId` to your backend with the action, and decide there after reading the stored event with a secret key. See [server-side verification](https://docs.fingerly.io/docs/server-side-verification).

## Configuration

**`Fingerly.load`**

- `apiKey` (String, required): Your public key. Its prefix decides the regional API.
- `endpoint` (String, default `""`): A custom API origin or first-party proxy. Empty uses the region in the key.
- `schedule` (ScheduleOptions, default `budgetMs 1200`): The collection budget: `budgetMs`, `defaultSourceTimeoutMs`, `concurrency` and `tiers`.
- `consent` (ConsentState, default `.granted`): `.granted`, `.pending` or `.denied`. Until it is `.granted`, `identify` and `collect` read and send nothing and throw `ConsentError`. Change it with `setConsent(_:)`. See [consent](https://docs.fingerly.io/docs/privacy-and-consent#consent).

**`identify`**

- `tag` (String?): Your own reference for this identification, such as `checkout:8412`.
- `tiers` ([SourceTier], default `all`): Which collection tiers to run: `.fast`, `.deferred`, or both.
- `submit` (Bool, default `true`): Set `false` to collect and compute verdicts without sending anything.

```swift Budget.swift
let fingerly = try await Fingerly.load(
    apiKey: "fly_pk_us_production_…",
    schedule: ScheduleOptions(budgetMs: 800, concurrency: 4)
)

// A screen that cannot wait: only the fast tier.
let result = try await fingerly.identify(tiers: [.fast])
```

## The result

**`IdentifyResult`**

- `requestId` (String): Identifies this identification. Send it to your server with the action it protects.
- `visitorId` (String): The stable identifier the server resolved for this device.
- `visitorIsNew` (Bool): Whether your organization is seeing this visitor for the first time.
- `visitorConfidence` (Int): How sure the identification is, from 0 to 100.
- `identifiable` (Bool): `false` when the device gave too little to identify anyone.
- `duplicate` (Bool): `true` when the server had already answered this request.
- `state` (String): `enriched`, or `unavailable` when the network lookup could not run.
- `suspectScore` (Int?): The server's weighted score. `nil` when nothing was scored, which is not the same as `0`.
- `suspectLevel` (String?): `low`, `medium` or `high`.
- `triggers` ([IdentifyTrigger]): The signals the server scored: `signal`, `group`, `weight` and `confidence`.
- `verdicts` (Verdicts): Local, advisory verdicts computed on the device.
- `report` (SignalReport): The report that was sent.

## Verdicts

| Verdict | What it means |
| --- | --- |
| `jailbreak` | The device is jailbroken. |
| `simulator` | The app is running in the iOS Simulator. |
| `instrumentation` | An instrumentation toolkit is attached to the app. |
| `mitm` | Something is intercepting the app's encrypted traffic. |
| `automation` | A debugger or a UI test runner is driving the app. |
| `tampering` | The app's code has been hooked or modified at runtime. |
| `farm` | The device looks mass-provisioned or freshly reset. |

Each verdict has `value`, `confidence` (`.low`, `.medium`, `.high`) and `reasons`. They are advisory. The server scores the same evidence with your [risk weights](https://docs.fingerly.io/docs/risk-weights).

```swift Verdicts.swift
if result.verdicts.jailbreak.value, result.verdicts.jailbreak.confidence == .high {
    // add friction here, and let your server make the final decision
}
```

## Optional: jailbreak app checks

No `Info.plist` usage description is required. To let the SDK check for well-known jailbreak apps, declare their URL schemes. The SDK only queries the schemes your app declares.

```xml Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
  <string>cydia</string>
  <string>sileo</string>
  <string>zbra</string>
  <string>filza</string>
</array>
```

## Errors

`identify` and `collect` throw `ConsentError` when consent is not `.granted`. `identify` throws a `TransportError` with `message`, `status` and `retryable`. The SDK already retries network failures, `429` and `5xx` up to three attempts with one idempotency key. `401` means the key is wrong; `402` means your organization is not accepting traffic.

```swift Errors.swift
do {
    let result = try await fingerly.identify(tag: "checkout")
} catch let error as TransportError where error.status == 401 {
    // the key is wrong or revoked
} catch {
    // proceed, and let your server treat the missing request ID as missing evidence
}
```

## Objective-C

`FingerlyBridge` exposes `configure(apiKey:endpoint:platform:)`, `identify(tag:submit:completion:)` and `collect(completion:)` to Objective-C. Errors use the domain `io.fingerly.sdk` with the HTTP status as the code.
