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
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.
Configuration
Fingerly.load
apiKeyStringrequiredYour public key. Its prefix decides the regional API.endpointStringDefault""A custom API origin or first-party proxy. Empty uses the region in the key.scheduleScheduleOptionsDefaultbudgetMs 1200The collection budget:budgetMs,defaultSourceTimeoutMs,concurrencyandtiers.consentConsentStateDefault.granted.granted,.pendingor.denied. Until it is.granted,identifyandcollectread and send nothing and throwConsentError. Change it withsetConsent(_:). See consent.
identify
tagString?Your own reference for this identification, such ascheckout:8412.tiers[SourceTier]DefaultallWhich collection tiers to run:.fast,.deferred, or both.submitBoolDefaulttrueSetfalseto collect and compute verdicts without sending anything.
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
requestIdStringIdentifies this identification. Send it to your server with the action it protects.visitorIdStringThe stable identifier the server resolved for this device.visitorIsNewBoolWhether your organization is seeing this visitor for the first time.visitorConfidenceIntHow sure the identification is, from 0 to 100.identifiableBoolfalsewhen the device gave too little to identify anyone.duplicateBooltruewhen the server had already answered this request.stateStringenriched, orunavailablewhen the network lookup could not run.suspectScoreInt?The server's weighted score.nilwhen nothing was scored, which is not the same as0.suspectLevelString?low,mediumorhigh.triggers[IdentifyTrigger]The signals the server scored:signal,group,weightandconfidence.verdictsVerdictsLocal, advisory verdicts computed on the device.reportSignalReportThe 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.
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.
<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.
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.