SDKs

iOS

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

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.

Install

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

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

Identify a device

SignInViewModel.swift
import Fingerly

// consent: .granted only once you have consent (or another lawful basis)
let fingerly = try await Fingerly.load(apiKey: "fly_pk_production_…", 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)
}

Load once and keep the client for the life of the app. identify runs on Swift concurrency and never touches the main thread. consent defaults to .pending, which collects nothing: load with .pending and call setConsent(.granted) from your consent tool, or pass .granted as here once you have consent. See consent tools.

Send requestId to your backend with the action, and decide there after reading the stored event with a secret key. See server-side verification.

Configuration

Fingerly.load

  • apiKeyStringrequired
    Your public key. Its prefix names the environment it submits to.
  • endpointStringDefault ""
    A custom API origin, to which /api/v1/ingest is appended. Empty uses https://api.fingerly.io. Proxy integrations serve the browser SDK, not apps.
  • scheduleScheduleOptionsDefault budgetMs 1200
    The collection budget: budgetMs, defaultSourceTimeoutMs, concurrency and tiers.
  • consentConsentStateDefault .pending
    .granted, .pending or .denied. Until it is .granted, identify and collect read and send nothing and throw ConsentError. Change it with setConsent(_:). See consent.

identify

  • tagString?
    Your own reference for this identification, such as checkout:8412.
  • tiers[SourceTier]Default all
    Which collection tiers to run: .fast, .deferred, or both.
  • submitBoolDefault true
    Set false to collect and compute verdicts without sending anything.
Budget.swift
let fingerly = try await Fingerly.load(
    apiKey: "fly_pk_production_…",
    consent: .granted,
    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

  • requestIdString
    Identifies this identification. Send it to your server with the action it protects.
  • visitorIdString
    The stable identifier the server resolved for this device.
  • visitorIsNewBool
    Whether your organization is seeing this visitor for the first time.
  • visitorConfidenceInt
    How sure the identification is, from 0 to 100.
  • identifiableBool
    false when the device gave too little to identify anyone.
  • duplicateBool
    true when the server had already answered this request.
  • stateString
    enriched, or unavailable when the network lookup could not run.
  • suspectScoreInt?
    The server's weighted score. nil when nothing was scored, which is not the same as 0.
  • suspectLevelString?
    low, medium or high.
  • triggers[IdentifyTrigger]
    The signals the server scored: signal, group, weight and confidence.
  • verdictsVerdicts
    Local, advisory verdicts computed on the device.
  • reportSignalReport
    The report that was sent.

Verdicts

VerdictWhat it means
jailbreakThe device is jailbroken.
simulatorThe app is running in the iOS Simulator.
instrumentationAn instrumentation toolkit is attached to the app.
mitmSomething is intercepting the app's encrypted traffic.
automationA debugger or a UI test runner is driving the app.
tamperingThe app's code has been hooked or modified at runtime.
farmThe 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.

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

The install ID

The SDK keeps one small record in the Keychain: a random install ID, when it was first seen and how many times the app has launched. It is kept only on this device, so it is never in a backup, and it survives the app being deleted, so a reinstall on the same device keeps its visitor ID. It is read and written only while consent is granted.

The server recognises the device by this install ID and nothing else. The device signals are used for the verdicts and the score, never to identify a device or a user.

Deleting the stored identity

DeleteAccount.swift
fingerly.setConsent(.denied)   // if nothing should be collected afterwards
try Fingerly.deleteStoredIdentity()

Deletes the install record, for erasure requests and account deletion. Because the record survives the app being deleted, this is the only way to remove it. It needs no client and no consent, sends nothing, and deleting a record that does not exist is not an error. A Keychain refusal throws StoredIdentityError with the Keychain status. The next identification made with consent starts a new install ID that nothing links to the old one. See account deletion and erasure.

App Store

  • The SDK ships a privacy manifest, PrivacyInfo.xcprivacy, in the Swift package and in the pod's Fingerly_Privacy resource bundle. Its only required-reason API is file timestamps, reason C617.1, for the age of your app's own container.
  • It does not track, so it needs no App Tracking Transparency prompt.
  • Do not ship it in Kids Category apps, and do not use the browser SDK inside a WKWebView in an iOS app; use this SDK instead.
  • See App Store and Google Play privacy for the App Privacy answers.

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.

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.

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:), collect(completion:) and deleteStoredIdentity() to Objective-C. Errors use the domain io.fingerly.sdk with the HTTP status as the code. deleteStoredIdentity() returns nil on success, or an error with code -3 and the Keychain status under userInfo["status"].