> ## Documentation Index
> Fetch the complete documentation index at: https://otpless.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS

> Integrate Device Fingerprint SDK in your iOS app.

<Info>
  The SDK requires iOS 15.0+ at runtime (deployment target 13.0+). Calls are guarded with `@available(iOS 15.0, *)` and are no-ops below iOS 15.
</Info>

## Prerequisites

* iOS 15.0 or higher at runtime
* App ID, Client ID, and Client Secret from the OTPless dashboard
* Swift 5.5–6.0, Xcode 14+

## Installation

**Swift Package Manager**

Add the package URL in Xcode → **File → Add Package Dependencies…**, or in your `Package.swift`:

```
https://github.com/otpless-tech/otpless-ios-intelligence-sdk
```

Dependency rule: **Up to Next Major Version** from `1.2.0`.

**CocoaPods**

```ruby Podfile theme={null}
platform :ios, '13.0'
use_frameworks!
pod 'OTPlessIntelligence', '~> 1.2.0'
```

Run `pod install` and open the generated `.xcworkspace`. `OtplessEventIO` is resolved transitively — no manual step.

## Add permissions

In your `Info.plist`, add the optional permissions:

```xml Info.plist theme={null}
<!-- Optional: enables GPS / geo-spoof signals -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to detect location spoofing and protect your account.</string>
```

Optional entitlements that improve signal accuracy (**Signing & Capabilities → + Capability**):

* **Access WiFi Information** — network identity signals.
* **iCloud → CloudKit** — cross-device identity signals.

The SDK never requests location itself. If geo signals matter, prompt the user before your first `fetchIntelligence`:

```swift theme={null}
import CoreLocation

CLLocationManager().requestWhenInUseAuthorization()
```

## Initialize the SDK

Call `initialize` once, as early as possible — in your `AppDelegate` or SwiftUI `@main init()`. Pass your App ID:

```swift AppDelegate.swift theme={null}
import OTPlessIntelligence

if #available(iOS 15.0, *) {
    OTPlessIntelligence.shared.initialize(appId: "<OTPLESS_APP_ID>") { success in
        // true  → SDK ready
        // false → check [OTPless] logs (debug builds only)
    }
}
```

`isInitialized: Bool` reflects whether `initialize` has completed successfully.

## Integrate with OtplessBM SDK

If you already use `OtplessBM`, you do **not** call `fetchIntelligence` yourself — set `deviceFingerprintMode` on the request and `OtplessBM` handles the fetch via runtime lookup. You still install `OTPlessIntelligence` and call `initialize(appId:)` at launch.

```swift theme={null}
import OtplessBM

let otplessRequest = OtplessRequest()
otplessRequest.set(phoneNumber: "PHONE_NUMBER", countryCode: "COUNTRY_CODE")
otplessRequest.set(deviceFingerprintMode: .SYNC)
Otpless.shared.startHeadless(withRequest: otplessRequest, ...)
```

### `DeviceFingerprintMode` values

| Value   | Behaviour                                                                                              |
| ------- | ------------------------------------------------------------------------------------------------------ |
| `NONE`  | Device fingerprinting is disabled. Default value.                                                      |
| `ASYNC` | Fingerprint is collected in parallel with the auth request. Does not add latency.                      |
| `SYNC`  | Fingerprint is collected before the auth request is sent. Ensures fingerprint data is always included. |

If `OTPlessIntelligence` is not linked or not initialized, `OtplessBM` logs a warning and the auth flow proceeds without a `dfrId` — failures never block auth.

## Fetch device intelligence

`fetchIntelligence` returns an `IntelligenceApiResponse` containing `dfrId`. Call it from a `Task` (async form) or via the ObjC-bridged callback.

```swift theme={null}
import OTPlessIntelligence

Task {
    do {
        let response = try await OTPlessIntelligence.shared.fetchIntelligence()
        // Send response.dfrId to your backend to fetch the full report.
    } catch {
        // Handle the error — degrade gracefully.
    }
}
```

Callback form (Swift or Objective-C):

```swift theme={null}
OTPlessIntelligence.shared.fetchIntelligence(params: nil, updateInfo: nil) {
    success, dfrId, intelligenceResponse, errorMessage in
    DispatchQueue.main.async {
        // Completion fires on a background thread.
    }
}
```

Retries: the engine call and backend push both use exponential backoff — 500 ms base, max 4 attempts. A response missing `dfrId` is treated as a failure and retried.

## Pass additional context (optional)

Pass `params` and/or `updateInfo` to attach extra context and enable more accurate risk scoring.

```swift theme={null}
let updateInfo = UpdateInfo(
    userId: "user-123",
    phoneNumber: "1234567890",
    phoneInputType: .manual,
    otpInputType: .autoFilled,
    userEventType: .transaction,
    merchantId: "merchant-id",
    additionalInput: [
        "TRANSACTION_ID": "txn-abc-123",
        "METHOD": "UPI",
        "AMOUNT": "500",
        "CURRENCY": "INR"
    ]
)

let response = try await OTPlessIntelligence.shared.fetchIntelligence(updateInfo: updateInfo)
```

In the callback / Objective-C form, `updateInfo` is a dictionary; enum fields expect raw-value strings (`"MANUAL"`, `"COPY_PASTED"`, `"LOGIN"`, …). Unknown keys and invalid enum strings are silently dropped.

### `UpdateInfo` fields

| Field             | Type                | Description                                                                      |
| ----------------- | ------------------- | -------------------------------------------------------------------------------- |
| `userId`          | `String?`           | Your internal user identifier. Used to correlate the result with a user account. |
| `phoneNumber`     | `String?`           | Customer phone number.                                                           |
| `phoneInputType`  | `PhoneInputType?`   | How the phone number was entered: `.manual`, `.copyPasted`, `.googleHint`.       |
| `otpInputType`    | `OtpInputType?`     | How the OTP was entered: `.manual`, `.copyPasted`, `.autoFilled`.                |
| `userEventType`   | `UserEventType?`    | The action being performed: `.login`, `.signup`, `.transaction`, `.others`.      |
| `merchantId`      | `String?`           | Merchant identifier, if applicable.                                              |
| `additionalInput` | `[String: String]?` | Arbitrary key-value pairs for custom context.                                    |

`params` is a per-call pass-through — every non-reserved entry is forwarded into the push body verbatim. Reserved keys (silently dropped): `data`, `status`, `requestId`, `message`, plus SDK-set `tsId`, `inId`, `platform`, `appId`.

## Get the detailed report (server-side)

After receiving `dfrId`, your **backend** exchanges it for the full report. Never call this from the app — it requires `CLIENT_SECRET`.

```bash theme={null}
curl --request POST \
  --url https://platform.otpless.app/client/v1/device-fingerprint \
  --header 'clientid: YOUR_CLIENT_ID' \
  --header 'clientsecret: YOUR_CLIENT_SECRET' \
  --header 'content-type: application/json' \
  --data '{ "dfrId": "DFRID_FROM_SDK", "deviceType": "IOS" }'
```

## Sample response

### Success

```json theme={null}
{
    "requestId": "403ad427-5018-47b9-b6e8-790e17a78201",
    "sessionId": "233sd769-9367-39h9-b9e8-650eb6e87820",
    "newDevice": false,
    "deviceId": "43fccb70-d64a-4c32-a251-f07c082d7034",
    "vpn": false,
    "proxy": false,
    "simulator": false,
    "jailbroken": false,
    "cloned": false,
    "geoSpoofed": false,
    "mirroredScreen": false,
    "hooking": false,
    "appTampering": false,
    "factoryReset": false,
    "factoryResetTime": 1743419662000,
    "ip": "106.219.161.71",
    "sessionRiskScore": 0.0,
    "deviceRiskScore": 0.0,
    "gpsLocation": { "latitude": 28.51, "longitude": 77.08, "altitude": 237.24 },
    "ipDetails": {
        "city": "New Delhi",
        "region": "National Capital Territory of Delhi",
        "country": "IN",
        "isp": "Reliance Jio Infocomm Limited",
        "asn": "55836",
        "fraudScore": 0.0
    },
    "deviceMeta": {
        "brand": "Apple",
        "model": "iPhone15,2",
        "iOSVersion": "17.4.1",
        "cpuType": "arm64",
        "screenResolution": "1179x2556",
        "totalRAM": "5368709120",
        "storageAvailable": "58363809792",
        "storageTotal": "113598214144"
    },
    "ruleAction": {
        "action": "WARN",
        "name": "VPN enabled",
        "description": "A VPN connection was detected on this device.",
        "message": "For security reasons, please disable your VPN and try again."
    },
    "appliedRules": {
        "rules": { "55 : VPN enabled": 0, "99 : App is tampered": 50 },
        "totalScore": 50.0
    },
    "additionalData": {}
}
```

### Error

```json theme={null}
{
  "requestId": "7e12d131-2d90-4529-a5c7-35f457d86ae6",
  "errorMessage": "OTPless server error"
}
```

## Response fields

| Field              | Type    | Description                                                                                      | Default |
| ------------------ | ------- | ------------------------------------------------------------------------------------------------ | ------- |
| `requestId`        | string  | Unique identifier for the request.                                                               | `""`    |
| `sessionId`        | string  | Unique identifier for the current app session.                                                   | `""`    |
| `newDevice`        | boolean | Whether this is the first time this device has been seen.                                        | `false` |
| `deviceId`         | string  | Stable identifier for the device.                                                                | `""`    |
| `sessionRiskScore` | float   | Risk score (0–100) based on the current session state.                                           | `0.0`   |
| `deviceRiskScore`  | float   | Risk score that also factors in the device's historical state across past sessions.              | `0.0`   |
| `vpn`              | boolean | A VPN is active on the device.                                                                   | `false` |
| `proxy`            | boolean | The device is behind a proxy server.                                                             | `false` |
| `simulator`        | boolean | The app is running on a simulator rather than physical hardware.                                 | `false` |
| `jailbroken`       | boolean | The device has been jailbroken.                                                                  | `false` |
| `cloned`           | boolean | The user is running a cloned instance of the app.                                                | `false` |
| `geoSpoofed`       | boolean | The device's location is being faked.                                                            | `false` |
| `mirroredScreen`   | boolean | The device's screen is being mirrored.                                                           | `false` |
| `hooking`          | boolean | The app has been altered by a hooking framework.                                                 | `false` |
| `appTampering`     | boolean | The app binary has been modified in an unauthorized way.                                         | `false` |
| `factoryReset`     | boolean | A suspicious factory reset has been performed.                                                   | `false` |
| `factoryResetTime` | long    | Timestamp of the last factory reset. `-1` if not detected.                                       | `-1`    |
| `ip`               | string  | Current IP address of the device.                                                                | `""`    |
| `ipDetails`        | object  | IP metadata including country, region, city, ISP, ASN, and fraud score.                          | `{}`    |
| `gpsLocation`      | object  | GPS location details including latitude, longitude, and altitude.                                | `{}`    |
| `deviceMeta`       | object  | Device hardware metadata: brand, model, OS version, screen resolution, storage, RAM, CPU.        | `{}`    |
| `ruleAction`       | object  | The triggered rule and recommended action (`ALLOW`, `WARN`, `BLOCK`) with a user-facing message. | `{}`    |
| `appliedRules`     | object  | Full list of evaluated rules, their scores, and the total rule score.                            | `{}`    |
| `additionalData`   | object  | Reserved for custom fields returned based on your account configuration.                         | `{}`    |

## Error reference

```swift theme={null}
public enum OTPlessIntelligenceError: Error {
    case notConfigured
    case intelligenceError(requestId: String, message: String)
    case unknown
}
```

| Case (async)                             | Callback `errorMessage`        | When it fires                                                    |
| ---------------------------------------- | ------------------------------ | ---------------------------------------------------------------- |
| `.notConfigured`                         | `"SDK not initialised"`        | `fetchIntelligence` called before `initialize` succeeded         |
| `.intelligenceError(requestId:message:)` | server-provided message        | Engine error, push failure, or `dfrId` missing after all retries |
| `.unknown`                               | `"Unknown intelligence error"` | Unexpected internal state                                        |

Intelligence signals are advisory — always degrade gracefully on failure. Log `requestId` for support.

## Troubleshooting

* **`initialize` returns `false`** — check `[OTPless]` logs in debug builds. Common causes: invalid App ID (401), App ID not found (404), no internet, or credentials rejected by `IdentityFraud`.
* **`fetchIntelligence` returns `.notConfigured`** — you called it before `initialize` completed. Wait for the completion callback with `success == true`.
* **`No such module 'OTPlessIntelligence'`** — CocoaPods: open `.xcworkspace`, re-run `pod install`. SPM: **File → Packages → Resolve Package Versions**. Then Clean Build Folder (`⇧⌘K`).
* **With `OtplessBM`, `dfrId` never arrives** — confirm `OTPlessIntelligence` is linked and `initialize(appId:)` runs before any `OtplessBM` request with mode `.SYNC` / `.ASYNC`. Look for `OTPLESS: OTPlessIntelligence not linked.` in the device log.
* **UI crash after `fetchIntelligence`** — completions fire on a background thread; wrap UI updates in `DispatchQueue.main.async`.
* **Simulator** — some signals (GPS, IP, hardware attestation) are unavailable or mocked. Validate on a real device before release.
