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

# iOS

> Install the Cr3dentials SDK in an iOS app and run a verification.

## Requirements

* iOS 14.0 or later. Devices are arm64. The simulator slice covers both Apple silicon and Intel Macs.
* Xcode 26 to upload to the App Store. The SDK is built and tested with Xcode 26.3 and declares Swift tools 5.9.
* The values from [What you need from us](/sdk/overview#what-you-need-from-us), and a backend that [creates SDK runs](/sdk/create-a-run).

The SDK needs no `Info.plist` key and no entitlement.

## Install

**Swift Package Manager**

In Xcode, choose **File → Add Package Dependencies** and enter `https://github.com/Cr3dentials/zktls-sdk-releases`. Add the product `ZktlsSDK` to your app target.

In a `Package.swift`:

```swift theme={null}
.package(url: "https://github.com/Cr3dentials/zktls-sdk-releases", from: "1.0.0"),
// in your target's dependencies:
.product(name: "ZktlsSDK", package: "zktls-sdk-releases"),
```

**CocoaPods**

The releases repository is its own spec source. Keep it first in your `Podfile`, because CocoaPods takes a pod from the first source that has it.

```ruby theme={null}
source 'https://github.com/Cr3dentials/zktls-sdk-releases.git'
source 'https://cdn.cocoapods.org/'
platform :ios, '14.0'

target 'YourApp' do
  pod 'ZktlsSDK', '~> 1.0'
end
```

The xcframework is signed with the Cr3dentials Apple Distribution certificate, Team ID `R8287QD4F7`. To check it, run `codesign -dv ZktlsSDKFFI.xcframework`.

## Build the run's configuration

Fetch the trust bundle and the policy bundle right before each run, and build one `TreeRunConfig` from them and from the run your backend issued:

```swift theme={null}
import ZktlsSDK

// Yours: what your backend returns after it creates the run.
struct IssuedRun: Decodable {
    let packId: String
    let resultUrl: String
    let accessToken: String
}

func treeConfig(for run: IssuedRun) async throws -> TreeRunConfig {
    let trustBundle = try await fetchText(Cr3d.trustBundleUrl)     // yours: a plain HTTPS GET
    let policyBundle = try await fetchText(Cr3d.policyBundleUrl)
    return TreeRunConfig(
        baseUrl: Cr3d.baseUrl,
        appCredential: Cr3d.appCredential,
        userIdHash: sha256Hex(currentUserId),                       // yours: never the raw id
        idempotencyKey: UUID().uuidString,                          // new for every run
        clientRegion: userCountry ?? "",                            // ISO 3166-1 alpha-2, or "" if unknown
        networkKey: "default",
        platformId: run.packId,
        trustBundleJson: trustBundle,
        trustBundleUrl: Cr3d.trustBundleUrl,
        trustOperatorKey: Cr3d.trustOperatorKey,
        policyBundleJson: policyBundle,
        policyBundleDigest: Cr3d.policyBundleDigest,
        relyingPartyNonceHex: "",
        resultUrl: run.resultUrl,
        resultToken: run.accessToken,
        paramsJson: "")                                             // a check's params as JSON text, or ""
}
```

| Field | Rules |
| - | - |
| `userIdHash` | Your own hash of your own user id. The same value every time for the same user, because it counts per-user quotas. At most 128 characters, so a hex SHA-256 fits |
| `idempotencyKey` | New for every run, never reused. A fresh UUID works |
| `clientRegion` | The user's country. Pass `""` if you don't know it, and the SDK measures the available regions and picks the fastest |
| `networkKey` | Any stable label. It never leaves the device. Don't read the Wi-Fi name for it, because that needs location permission |
| `trustBundleUrl` | The URL that always serves the latest bundle. If an enclave rotated its key since your copy, the SDK refetches the bundle once instead of failing the run |

## Run a verification

```swift theme={null}
import UIKit
import ZktlsSDK

final class VerifyViewController: UIViewController {
    private var client: ZktlsClient?

    func verify(platformId: Int) async {
        guard client == nil else { return }                          // one run at a time
        let client = ZktlsClient()                                    // a new client for every run
        self.client = client
        defer { self.client = nil }

        let lifecycle = ZkTlsLifecycle(host: UIApplication.shared, handle: client.lifecycleHandle)
        lifecycle.onExpiry = { client.cancel() }                      // iOS ended the background time
        defer { lifecycle.end() }                                     // every path, including failure
        await client.onProgress { phase, percent in
            if phase == .attesting { lifecycle.begin() }              // keeps the hand-off alive if the user leaves
            Task { @MainActor in /* update your progress view */ }
        }

        do {
            let run = try await backend.createSdkRun(platformId: platformId)   // yours
            let config = try await treeConfig(for: run)
            let host = TreeWebViewHost(presentingFrom: self, disclosure: disclosure)
            let summary = try await client.runTree(config: config, host: host)
            if summary.handedOff {
                show("You can close the app. We'll let you know when it's done.")
            } else if summary.succeeded && summary.delivered {
                show("Verified.")
            } else {
                show("Not verified. Please try again.")              // summary.error says why
            }
        } catch let e as SdkError {
            show(message(for: e))                                     // see Errors; never retry automatically
        } catch {
            show("Something went wrong. Please try again.")
        }
    }

    func cancelTapped() { client?.cancel() }
}

let disclosure = TreeDisclosure(
    title: "Connect your account",
    message: "We'll open the platform's own sign-in page. You sign in there yourself, and we never see "
        + "your password. Then we read the data this verification needs from your account.",
    accept: "Continue",
    decline: "Not now")
```

`backend`, `show` and `message(for:)` are yours. [Errors](/sdk/errors) has a `message(for:)` that covers every case.

When `runTree` throws, or returns with neither `delivered` nor `handedOff`, nothing reached Cr3dentials. Tell your backend so it [terminates the run](/sdk/create-a-run#hand-the-run-to-your-app).

## What `runTree` returns

Your app never sees the platform's data. `TreeSummary` only tells you how the run ended:

| Field | Meaning |
| - | - |
| `succeeded` | The pack's steps succeeded. For a platform with attested data, the attester also took the run |
| `error` | Why the steps failed, in the pack's words. For example, the user did not sign in |
| `sources` | Each observed source's key, with how many rows and pages it read |
| `delivered`, `deliveryError` | Whether the phone's result reached Cr3dentials, and if not, why |
| `handedOff` | The attester accepted the run and will post the result itself. The user may close the app |
| `isolation` | `ephemeral`: the run had its own non-persistent web data store. Empty if the page never opened |

## Things to know

* **The disclosure.** `TreeWebViewHost` shows your `TreeDisclosure` before it opens any page. If the user declines, nothing opens. Write it in your own words and translate it.
* **Where the page may go.** The WebView stays on the platform's domains and on the third-party sign-in providers the pack lists. If the user follows a link anywhere else, the run ends with `SdkError.Policy("navigation_refused: <host>")`.
* **Your app's web data.** Each run has its own non-persistent data store, so your own WebViews and cookies are untouched.
* **Background time.** Begin `ZkTlsLifecycle` when the `attesting` phase starts, and let `lifecycle.end()` run on every path. iOS usually grants about 30 seconds in the background. The hand-off takes seconds and at most 120 seconds. If iOS ends the time first, `onExpiry` cancels the run, and the user starts again.
* **Cancelling.** `cancel()` ends `runTree` with `SdkError.Cancelled` and closes the page. Cancelling the `Task` does the same, and so does the user closing the page.
* **Threads.** `runTree` runs on the SDK's own threads, so call it from any task. `onProgress` calls you on an SDK thread. Hop to the main actor for UI, and don't block in the callback.
* **Logging.** Log error cases and their text only. Never log the run's `accessToken`, cookies or your app credential.

## Before you submit

The SDK includes its privacy manifest. It declares User ID and Coarse Location, both linked to the user and used for App Functionality, and no tracking. Add these to your App Privacy answers, along with the data types your verification collects. Work through the [release checklist](/sdk/release-checklist) for export compliance and App Review notes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.