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

# iOS SDK

> Native iOS Swift SDK for KYC, KYB, biometric liveness, and AML. NFC passport reading, on-device capture. Pay-per-call from $0.30, 500 free/month.

export const AgentPromptAccordion = ({prompt, title = "AI Agent Integration Prompt"}) => {
  const [copied, setCopied] = React.useState(false);
  const handleCopy = e => {
    e.stopPropagation();
    if (!prompt) return;
    navigator.clipboard.writeText(prompt.trim()).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };
  const agents = ["Claude Code", "Codex", "Cursor", "Devin", "Windsurf", "GitHub Copilot"];
  return <div className="didit-agent-card">
      {}
      <div className="didit-agent-titlebar">
        <div className="didit-agent-dots" aria-hidden="true">
          <span className="didit-agent-dot didit-agent-dot-red"></span>
          <span className="didit-agent-dot didit-agent-dot-yellow"></span>
          <span className="didit-agent-dot didit-agent-dot-green"></span>
        </div>
        <span className="didit-agent-filename">{title}</span>
        <button type="button" className={`didit-agent-copy ${copied ? "didit-agent-copy-copied" : ""}`} onClick={handleCopy} title="Copy prompt to clipboard" aria-label={copied ? "Copied!" : "Copy prompt to clipboard"}>
          {copied ? <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <span>Copied</span>
            </> : <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
                <path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" stroke="currentColor" strokeWidth="1.5" />
              </svg>
              <span>Copy</span>
            </>}
        </button>
      </div>

      {}
      <pre className="didit-agent-body"><code>{prompt.trim()}</code></pre>

      {}
      <div className="didit-agent-footer">
        <span className="didit-agent-footer-label">Paste into</span>
        <div className="didit-agent-chips">
          {agents.map(name => <span key={name} className="didit-agent-chip">{name}</span>)}
        </div>
      </div>
    </div>;
};

<AgentPromptAccordion
  title="iOS SDK Integration Prompt"
  prompt={`Integrate Didit identity verification into my iOS app using the native Swift SDK (DiditSDK 3.6.0).

## Requirements
- iOS 13.0+, Xcode 15.0+, Swift 5.9+
- NFC chip reading requires iOS 15+ and a compatible device with NFC hardware
- The default install (DiditSDK / DiditSDK/Full) includes NFC and requires iOS 15.0+
- The no-NFC install (DiditSDKCore / DiditSDK/Core) supports iOS 13.0+

## Installation

### Option A: Swift Package Manager (recommended)
1. In Xcode: File > Add Package Dependencies
2. Enter URL: https://github.com/didit-protocol/sdk-ios.git
3. Select version 3.6.0 (or "Up to Next Major") and click Add Package
4. Add the product:
- Full (with NFC): product "DiditSDK"
- Core (no NFC):   product "DiditSDKCore"

In Package.swift:
.package(url: "https://github.com/didit-protocol/sdk-ios.git", from: "3.6.0")
Then add either .product(name: "DiditSDK", package: "DiditSDK") or .product(name: "DiditSDKCore", package: "DiditSDK").

### Option B: CocoaPods (binary podspec — DiditSDK is not on the public Trunk)
# Full (NFC, iOS 15+):
platform :ios, '15.0'
pod 'DiditSDK', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec'

# Core (no NFC, iOS 13+):
platform :ios, '13.0'
pod 'DiditSDK/Core', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec'

Then run: pod install
Open the .xcworkspace (not .xcodeproj).

On Xcode 15+ if you hit rsync "Operation not permitted", set Build Setting ENABLE_USER_SCRIPT_SANDBOXING to No on the project (Debug and Release).

## Required Permissions (Info.plist)
- NSCameraUsageDescription (required) — Document scanning and face verification
- NSMicrophoneUsageDescription (required) — Video recording for liveness checks
- NSPhotoLibraryUsageDescription (required) — Upload documents from gallery
- NFCReaderUsageDescription (only if using the Full SDK) — Read NFC chips in passports/IDs
- NSLocationWhenInUseUsageDescription (optional) — Geolocation for fraud prevention

## NFC Configuration (Full SDK only — skip when installing DiditSDK/Core or DiditSDKCore)
1. Add "Near Field Communication Tag Reading" capability in Xcode > Signing & Capabilities
2. Add ISO7816 identifiers to Info.plist:
com.apple.developer.nfc.readersession.iso7816.select-identifiers:
["D23300000045737445494420763335", "A0000002471001", "A0000002472001", "00000000000000"]
3. Add to entitlements file:
com.apple.developer.nfc.readersession.formats: ["TAG"]

## Backend: Create Session (your server)
POST https://verification.didit.me/v3/session/
Headers: { "x-api-key": DIDIT_API_KEY, "Content-Type": "application/json" }
Body: { "workflow_id": DIDIT_WORKFLOW_ID, "vendor_data": "user-id" }
Response: { "session_id": "uuid", "session_token": "short-token", "url": "https://verify.didit.me/session/short-token", "status": "Not Started" }
Send session_token to the iOS app (never expose DIDIT_API_KEY to the device).

## SwiftUI Integration (recommended)
import SwiftUI
import DiditSDK

struct ContentView: View {
var body: some View {
    Button("Verify Identity") {
        // Option A: With session token from your backend (recommended)
        DiditSdk.shared.startVerification(token: "session-token-from-backend")

        // Option B: With workflow ID (SDK creates session internally — UniLink, vendorData only)
        // DiditSdk.shared.startVerification(workflowId: "your-workflow-id", vendorData: "user-id")
    }
    .diditVerification { result in
        switch result {
        case .completed(let session):
            // session.status: VerificationStatus = .approved | .pending | .declined
            if session.status == .approved { /* grant access */ }
        case .cancelled(let session):
            // User dismissed the flow; session may be nil if none was created
            _ = session
        case .failed(let error, _):
            // VerificationError — show error.localizedDescription
            _ = error
        }
    }
}
}

## Programmatically dismissing the verification (3.6.0+)
Call DiditSdk.shared.dismiss() to end an active verification (e.g. on app backgrounding).
It dismisses the UI, resets state, and fires the .diditVerification handler with .cancelled(session:).
Do NOT toggle DiditSdk.shared.isPresented = false or call UIKit dismiss() on the topmost VC — that bypasses the completion pipeline.

## Result Statuses (SDK enum / API string)
- .approved / "Approved"  — user identity verified, grant access
- .declined / "Declined"  — verification failed, show retry or contact support
- .pending  / "In Review" — needs manual review by your compliance team

## Full Verification Results
The SDK only returns the status. Full decision data (document fields, face match scores, AML hits, etc.) arrives via webhook to your backend.
Set up webhooks: https://docs.didit.me/integration/webhooks

## Rate Limits (session creation)
- 600 session-create requests / minute per x-api-key (see https://docs.didit.me/integration/rate-limiting)
- On 429, retry honouring Retry-After / X-RateLimit-* headers

## Environment Variables (backend only — never expose in the iOS app)
- DIDIT_API_KEY — from Didit Console > API & Webhooks
- DIDIT_WORKFLOW_ID — from Didit Console > Workflows

## App Store Note
If you ship the Full (NFC) SDK, Apple may request an NFC demo video during review. Download: https://business.didit.me/videos/passport-nfc.mp4

## Docs
- iOS SDK: https://docs.didit.me/integration/native-sdks/ios-sdk
- GitHub: https://github.com/didit-protocol/sdk-ios
- Webhooks: https://docs.didit.me/integration/webhooks
- API Reference: https://docs.didit.me/api-reference/overview`}
/>

A lightweight, server-driven iOS SDK for identity verification with minimal configuration required. **Latest version: 3.6.0.**

<Card title="GitHub Repository" icon="github" href="https://github.com/didit-protocol/sdk-ios">
  View source code, releases, and the complete changelog on GitHub
</Card>

<Note>
  ### **Native SDK**: This is the recommended approach for iOS apps. Native SDKs provide the best user experience, optimized camera handling, and full NFC support.
</Note>

***

## Requirements

| Requirement | Minimum Version |
| ----------- | --------------- |
| iOS         | 13.0+           |
| Xcode       | 15.0+           |
| Swift       | 5.9+            |

### iOS Version Compatibility

| iOS Version     | Features Available                           |
| --------------- | -------------------------------------------- |
| iOS 13.0 - 14.x | All features **except** NFC passport reading |
| iOS 15.0+       | All features including NFC passport reading  |

***

## Installation

The SDK ships in two flavors:

| Product  | CocoaPods       | SwiftPM product | Includes                                                                              | Minimum iOS |
| -------- | --------------- | --------------- | ------------------------------------------------------------------------------------- | ----------- |
| **Full** | `DiditSDK`      | `DiditSDK`      | All features, including NFC passport reading                                          | iOS 15.0+   |
| **Core** | `DiditSDK/Core` | `DiditSDKCore`  | All features **except** NFC. No `OpenSSL.xcframework`, no CoreNFC runtime requirement | iOS 13.0+   |

Pick **Full** if your workflow scans the NFC chip on passports/eIDs. Pick **Core** if you only need document + face + liveness and want to avoid the NFC binary, CoreNFC runtime, and the App Store NFC demo-video review request.

### Swift Package Manager (Recommended)

Add the package to your project using Xcode:

1. Go to **File > Add Package Dependencies**
2. Enter the repository URL:

```
https://github.com/didit-protocol/sdk-ios.git
```

3. Select the version (or **Up to Next Major** from `3.6.0`) and click **Add Package**
4. When prompted to choose a product, pick **`DiditSDK`** (Full, NFC) or **`DiditSDKCore`** (no NFC)

Or in your `Package.swift`:

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/didit-protocol/sdk-ios.git", from: "3.6.0")
]
```

Then add one of these products to your target:

```swift theme={null}
// Full SDK (includes NFC passport reading)
.product(name: "DiditSDK", package: "DiditSDK")

// Core SDK (no NFC dependencies)
.product(name: "DiditSDKCore", package: "DiditSDK")
```

### CocoaPods

`DiditSDK` is distributed as a binary podspec hosted in the repo (it is not on the public CocoaPods Trunk), so you must reference the podspec URL.

**Full SDK** (with NFC, iOS 15.0+):

```ruby theme={null}
platform :ios, '15.0'

pod 'DiditSDK', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec'
```

**Core SDK** (no NFC, iOS 13.0+):

```ruby theme={null}
platform :ios, '13.0'

pod 'DiditSDK/Core', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec'
```

Then run:

```bash theme={null}
pod install
```

<Note>
  After `pod install`, open the generated `.xcworkspace` (not `.xcodeproj`).
</Note>

<Note>
  **Xcode 15+ rsync errors**: If you see `Operation not permitted` rsync errors during build, set **Build Settings → User Script Sandboxing** (`ENABLE_USER_SCRIPT_SANDBOXING`) to **No** on the project for both Debug and Release.
</Note>

### Manual (XCFramework)

Download the frameworks from the [GitHub Releases](https://github.com/didit-protocol/sdk-ios/releases) page, then drag the `.xcframework` folders into Xcode and set **Embed & Sign**.

* Full SDK: `DiditSDK.xcframework.zip` + `OpenSSL.xcframework.zip`
* Core SDK: `DiditSDK-Core.xcframework.zip`

***

## Permissions

The SDK requires the following permissions. Add these to your app's `Info.plist`:

| Permission    | Info.plist Key                        | Description                             | Required         |
| ------------- | ------------------------------------- | --------------------------------------- | ---------------- |
| Camera        | `NSCameraUsageDescription`            | Document scanning and face verification | ✅ Yes            |
| Microphone    | `NSMicrophoneUsageDescription`        | Video recording for liveness checks     | ✅ Yes            |
| Photo Library | `NSPhotoLibraryUsageDescription`      | Upload documents from device gallery    | ✅ Yes            |
| NFC           | `NFCReaderUsageDescription`           | Read NFC chips in passports/ID cards    | ⚠️ Full SDK only |
| Location      | `NSLocationWhenInUseUsageDescription` | Geolocation for fraud prevention        | ❌ Optional       |

### Example Info.plist Entries

```xml theme={null}
<key>NSCameraUsageDescription</key>
<string>Camera access is required to scan your identity documents for verification.</string>

<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required to record video for liveness verification.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access is required to upload document images.</string>

<key>NFCReaderUsageDescription</key>
<string>NFC access is required to read the chip in your identity document.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access helps verify your identity and prevent fraud.</string>
```

### NFC Configuration

Required only when installing the **Full** SDK. Skip this section if you installed `DiditSDK/Core` (CocoaPods) or `DiditSDKCore` (SwiftPM).

To enable NFC reading for passports and ID cards with chips:

1. **Add NFC Capability** in Xcode:
   * Select your target → **Signing & Capabilities** → **+ Capability** → **Near Field Communication Tag Reading**

2. **Add ISO7816 Identifiers** to `Info.plist`:

```xml theme={null}
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>D23300000045737445494420763335</string>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
</array>
```

3. **Add Entitlements** (in your `.entitlements` file):

```xml theme={null}
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
    <string>TAG</string>
</array>
```

<Warning>
  **Simulator Limitation (Full SDK only)**: The Full SDK links CoreNFC, and since Xcode 12 `libnfshared.dylib` is missing from simulators. See [this Stack Overflow thread](https://stackoverflow.com/questions/63915728/xcode12-corenfc-simulator-library-not-loaded) for a workaround. This does **not** apply when installing the Core SDK (`DiditSDK/Core` or `DiditSDKCore`). Test NFC features on physical devices only.
</Warning>

<Note>
  ### **App Store Review (Full SDK only)**: If you install the Full SDK, Apple may request a demo video during review because NFC-related code is part of the SDK binary — even if your workflow does not use NFC. Download our NFC demo video to submit to Apple: [Download NFC Demo Video](https://business.didit.me/videos/passport-nfc.mp4). This does not apply to the Core SDK.
</Note>

***

## Quick Start

### SwiftUI Integration

```swift theme={null}
import SwiftUI
import DiditSDK

struct ContentView: View {
    var body: some View {
        Button("Verify Identity") {
            // Method 1: UniLink — no backend required
            DiditSdk.shared.startVerification(workflowId: "your-workflow-id")
            
            // Method 2: Backend Session — full parameter control
            // DiditSdk.shared.startVerification(token: "your-session-token")
        }
        .diditVerification { result in
            handleResult(result)
        }
    }
    
    private func handleResult(_ result: VerificationResult) {
        switch result {
        case .completed(let session):
            print("Completed: \(session.status)")
        case .cancelled(let session):
            print("Cancelled")
        case .failed(let error, _):
            print("Failed: \(error.localizedDescription)")
        }
    }
}
```

### UIKit Integration

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

class VerificationViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Start verification when ready
        DiditSdk.shared.startVerification(token: "your-session-token")
    }
}
```

***

## Integration Methods

The SDK supports two integration methods:

### Method 1: UniLink (Simplest)

No backend required. The SDK creates the session directly using your workflow ID from the [Didit Console](https://business.didit.me). The UniLink method (`startVerification(workflowId:)`) only supports `vendorData`; for any other session parameters use Method 2.

<Note>
  For `vendorData` to be attached to the session via UniLink, enable the **Vendor Data** option in the [Didit Console](https://business.didit.me).
</Note>

```swift theme={null}
DiditSdk.shared.startVerification(
    workflowId: "your-workflow-id",
    vendorData: "user-123"
)
```

### Method 2: Backend Session (Recommended for Production)

Your backend creates the session via the [Create Verification Session API](/sessions-api/create-session) (`POST /v3/session/`) with full parameter support (`contact_details`, `expected_details`, `metadata`, `callback`, etc.), then passes the `session_token` to the SDK.

```swift theme={null}
// Your backend creates a session and returns the token
let sessionToken = await yourBackend.createVerificationSession(userId: currentUser.id)

// Pass the token to the SDK
DiditSdk.shared.startVerification(token: sessionToken)
```

This approach gives you full control over:

* Associating sessions with your users (`vendor_data`)
* Setting contact details and expected details for cross-validation
* Setting custom metadata
* Configuring callbacks per session
  This data (contact details, expected details, metadata, callback) is sent to the [Create Verification Session API](/sessions-api/create-session).

***

## Configuration

Customize the SDK behavior with `DiditSdk.Configuration`:

```swift theme={null}
let configuration = DiditSdk.Configuration(
    languageLocale: .spanish,      // Force Spanish language
    fontFamily: "Avenir",          // Custom font (must be registered in your app)
    loggingEnabled: true,          // Enable debug logging
    showCloseButton: true,         // Show close (X) button on step screens
    showExitConfirmation: true,    // Show confirmation dialog when user taps close
    closeOnComplete: false         // Don't auto-dismiss on completion
)

DiditSdk.shared.startVerification(
    token: "your-session-token",
    configuration: configuration
)
```

### Configuration Options

| Property               | Type                 | Default       | Description                                                                               |
| ---------------------- | -------------------- | ------------- | ----------------------------------------------------------------------------------------- |
| `languageLocale`       | `SupportedLanguage?` | Device locale | Force a specific language                                                                 |
| `fontFamily`           | `String?`            | System font   | Custom font family name (must be registered in your app via `UIAppFonts` in `Info.plist`) |
| `loggingEnabled`       | `Bool`               | `false`       | Enable SDK debug logging                                                                  |
| `showCloseButton`      | `Bool`               | `true`        | Show close (X) button on verification step screens                                        |
| `showExitConfirmation` | `Bool`               | `true`        | Show confirmation dialog when user attempts to exit                                       |
| `closeOnComplete`      | `Bool`               | `false`       | Auto-dismiss verification UI when complete (Web SDK equivalent: `closeModalOnComplete`)   |

<Info>
  **Theming & Colors**: Colors, backgrounds, and intro screen settings are configured through your [White Label settings](https://business.didit.me) in the Didit Console, not in the SDK configuration. This ensures consistent branding across all platforms.
</Info>

<Info>
  Options `showCloseButton`, `showExitConfirmation`, and `closeOnComplete` match the Web SDK's `DiditSdkConfiguration`. Mobile-specific options `languageLocale` and `fontFamily` exist because the mobile SDK renders the full verification UI natively (unlike the Web SDK which delegates to the hosted frontend inside an iframe).
</Info>

***

## Language Support

The SDK supports **53 languages**. If no language is specified, the SDK uses the device locale with English as fallback.

```swift theme={null}
// Use device locale (default)
let config = DiditSdk.Configuration()

// Force specific language
let config = DiditSdk.Configuration(languageLocale: .french)

// Detect device locale programmatically
let deviceLanguage = SupportedLanguage.fromDeviceLocale()
```

**[View All Supported Languages →](/integration/supported-languages)**

***

## Advanced Session Parameters

For advanced session parameters (`contact_details`, `expected_details`, `metadata`, `callback`), use the **Backend Session** method. Your backend calls the [Create Verification Session API](/sessions-api/create-session) with full parameters, then passes the `session_token` to the SDK.

***

## Handling Results

The `VerificationResult` enum provides the outcome of the verification:

### Result Cases

| Case                      | Description                                                     |
| ------------------------- | --------------------------------------------------------------- |
| `.completed(session:)`    | Verification flow completed (check `session.status` for result) |
| `.cancelled(session:)`    | User cancelled the verification flow                            |
| `.failed(error:session:)` | An error occurred during verification                           |

### SessionData Properties

| Property       | Type                 | Description                             |
| -------------- | -------------------- | --------------------------------------- |
| `sessionId`    | `String`             | Unique session identifier               |
| `status`       | `VerificationStatus` | `.approved`, `.pending`, or `.declined` |
| `country`      | `String?`            | Country code (ISO 3166-1 alpha-3)       |
| `documentType` | `String?`            | Document type used for verification     |

### Error Types

| Error                 | Description                   |
| --------------------- | ----------------------------- |
| `.sessionExpired`     | The session has expired       |
| `.networkError`       | Network connectivity issue    |
| `.cameraAccessDenied` | Camera permission not granted |
| `.unknown(String)`    | Other error with message      |

### Complete Result Handling Example

```swift theme={null}
.diditVerification { result in
    switch result {
    case .completed(let session):
        switch session.status {
        case .approved:
            print("✅ Approved! Session: \(session.sessionId)")
            // User is verified - grant access
            
        case .pending:
            print("⏳ Under review. Session: \(session.sessionId)")
            // Show "verification in progress" UI
            
        case .declined:
            print("❌ Declined. Session: \(session.sessionId)")
            // Handle declined verification
        }
        
    case .cancelled(let session):
        if let session = session {
            print("🚫 Cancelled session: \(session.sessionId)")
        }
        // User chose to cancel - maybe show retry option
        
    case .failed(let error, let session):
        print("⚠️ Error: \(error.localizedDescription)")
        // Handle error - show retry or contact support
    }
}
```

***

## Dismissing the Verification Programmatically

<Note>
  Available in DiditSDK **3.6.0+**.
</Note>

The host app can end an active verification programmatically with `DiditSdk.shared.dismiss()`. This is the recommended way to tear down the verification when the host needs to take over the screen — for example, when the app moves to the background.

```swift theme={null}
DiditSdk.shared.dismiss()
```

`dismiss()` goes through the SDK's normal completion pipeline: it dismisses the presented UI, resets internal state, and invokes the `.diditVerification` handler with `.cancelled(session:)` carrying the current `sessionId` if a session was created. It is a no-op when no verification is active.

### Example: dismiss when the app backgrounds

```swift theme={null}
NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification,
    object: nil,
    queue: .main
) { _ in
    DiditSdk.shared.dismiss()
}
```

<Warning>
  Do **not** set `DiditSdk.shared.isPresented = false` to dismiss — the flag only triggers presentation on its rising edge and setting it to `false` is a no-op. Likewise, calling UIKit's `dismiss(animated:)` on the topmost view controller is not supported: it bypasses the SDK's completion pipeline so your `.diditVerification` handler is never fired.
</Warning>

***

## Observing SDK State

You can observe the SDK state for custom loading UI:

```swift theme={null}
struct CustomView: View {
    @ObservedObject private var sdk = DiditSdk.shared
    
    var body: some View {
        VStack {
            switch sdk.state {
            case .idle:
                Text("Ready to verify")
            case .creatingSession:
                ProgressView("Creating session...")
            case .loading:
                ProgressView("Loading...")
            case .ready:
                Text("Verification in progress")
            case .error(let message):
                Text("Error: \(message)")
            }
        }
    }
}
```

***

## End-to-End Example (Backend Session → iOS SDK → Result)

This pattern is the production-ready integration. Your backend creates the session, your iOS app receives the `session_token`, and the SDK runs the flow.

### 1. Backend — create the session

```ts theme={null}
// Node.js / Express example. Run on your server. NEVER ship DIDIT_API_KEY to the device.
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/verification/start", async (req, res) => {
  const response = await fetch("https://verification.didit.me/v3/session/", {
    method: "POST",
    headers: {
      "x-api-key": process.env.DIDIT_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      workflow_id: process.env.DIDIT_WORKFLOW_ID,
      vendor_data: req.body.userId, // your stable user id
    }),
  });

  if (!response.ok) {
    return res.status(500).json({ error: `Didit ${response.status}` });
  }

  const { session_id, session_token } = await response.json();
  // Persist session_id <-> userId so you can match the webhook later.
  res.json({ session_id, session_token });
});
```

### 2. iOS — exchange and start the SDK

```swift theme={null}
import SwiftUI
import DiditSDK

struct VerifyButton: View {
    let userId: String
    @State private var error: String?

    var body: some View {
        Button("Verify Identity") {
            Task { await start() }
        }
        .diditVerification { result in
            switch result {
            case .completed(let session) where session.status == .approved:
                // Optimistically reflect success in the UI; rely on the webhook
                // delivered to your backend as the source of truth.
                break
            case .completed(let session):
                print("Terminal status: \(session.status.rawValue)")
            case .cancelled:
                print("User cancelled")
            case .failed(let err, _):
                error = err.localizedDescription
            }
        }
    }

    private func start() async {
        do {
            var req = URLRequest(url: URL(string: "https://your-backend.example.com/api/verification/start")!)
            req.httpMethod = "POST"
            req.setValue("application/json", forHTTPHeaderField: "Content-Type")
            req.httpBody = try JSONEncoder().encode(["userId": userId])

            let (data, _) = try await URLSession.shared.data(for: req)
            struct Payload: Decodable { let session_token: String }
            let payload = try JSONDecoder().decode(Payload.self, from: data)

            await MainActor.run {
                DiditSdk.shared.startVerification(token: payload.session_token)
            }
        } catch {
            self.error = error.localizedDescription
        }
    }
}
```

### 3. Backend — receive the final decision via webhook

The SDK result is convenient for UI feedback, but the authoritative outcome arrives via webhook. See the [Webhooks guide](/integration/webhooks) for HMAC verification.

```ts theme={null}
app.post("/api/webhooks/didit", express.raw({ type: "application/json" }), (req, res) => {
  // 1. Verify the X-Signature header (see Webhooks docs)
  // 2. Parse the body
  const event = JSON.parse(req.body.toString());
  if (event.webhook_type === "status.updated") {
    // event.session_id, event.status ("Approved" | "Declined" | "In Review" | ...)
    // Look up the user by session_id and update their verification state.
  }
  res.sendStatus(200);
});
```

***

## Complete SwiftUI Example

```swift theme={null}
import SwiftUI
import DiditSDK

struct HomeView: View {
    @State private var resultMessage: String?
    @State private var isVerified = false
    
    var body: some View {
        VStack(spacing: 24) {
            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .font(.system(size: 64))
                    .foregroundColor(.green)
                Text("Identity Verified")
                    .font(.title2)
            } else {
                Button("Verify Identity") {
                    startVerification()
                }
                .font(.headline)
                .padding(.horizontal, 32)
                .padding(.vertical, 16)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(12)
            }
            
            if let message = resultMessage {
                Text(message)
                    .foregroundColor(.secondary)
                    .multilineTextAlignment(.center)
                    .padding()
            }
        }
        .diditVerification { result in
            handleResult(result)
        }
    }
    
    private func startVerification() {
        let config = DiditSdk.Configuration(
            languageLocale: .english
        )
        
        // Method 1: UniLink — no backend required
        DiditSdk.shared.startVerification(
            workflowId: "your-workflow-id",
            vendorData: "user-123",
            configuration: config
        )
        
        // Method 2: Backend Session — full parameter control
        // DiditSdk.shared.startVerification(
        //     token: "your-session-token",
        //     configuration: config
        // )
    }
    
    private func handleResult(_ result: VerificationResult) {
        switch result {
        case .completed(let session):
            if session.status == .approved {
                isVerified = true
            }
            resultMessage = """
                Status: \(session.status.rawValue)
                Session: \(session.sessionId)
                Country: \(session.country ?? "N/A")
                Document: \(session.documentType ?? "N/A")
                """
                
        case .cancelled(let session):
            resultMessage = "Cancelled - Session: \(session?.sessionId ?? "unknown")"
            
        case .failed(let error, _):
            resultMessage = "Failed: \(error.localizedDescription)"
        }
    }
}
```

***
