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

# iOS SDK

> Add Clarity's in-app support chat to an iOS app: install, initialize, identify users, push notifications.

The iOS SDK shows Clarity's support chat as a native bottom sheet or full-screen view. It pre-warms the chat in the background so it opens instantly, reports new replies for your own unread badge, and never crashes your app: every failure is reported to you instead.

**Requirements:** iOS 15 or later, Swift 5.10 or later (Xcode 15.3 or later). Works in UIKit and SwiftUI apps. No third-party dependencies.

## Get your credentials

In the cabinet, go to **Settings > Mobile SDK**, click **Add Integration**, choose **iOS** and enter your app's bundle identifier as the **App ID**. The integration dialog shows:

| Value | Where it goes |
| - | - |
| Organization Id | `ClarityConfig.organizationId` |
| Integration key | `ClarityConfig.integrationKey` |
| Live chat host | `ClarityConfig.baseUrl`: `https://live-chat.leap.onclarity.com` |

The channel's **App ID** must equal your bundle identifier; the SDK sends it when the chat session starts and a mismatch is refused. These values are identifiers, not secrets, so shipping them in the app is fine.

## Install

**Xcode**

1. In Xcode, choose **File > Add Package Dependencies…**
2. Paste `https://github.com/Anecdote-AI/clarity-ios-sdk`.
3. Set the dependency rule to **Up to Next Major Version**, from `1.3.0`.
4. Add the **ClaritySDK** product to your app target.

**Package.swift**

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/Anecdote-AI/clarity-ios-sdk", .upToNextMajor(from: "1.3.0")),
],
targets: [
    .target(name: "YourApp", dependencies: [
        .product(name: "ClaritySDK", package: "clarity-ios-sdk"),
    ]),
]
```

**CocoaPods**

```ruby theme={null}
pod 'ClaritySDK', '~> 1.3'
```

Then run `pod install`. The pod is built from the same source and versions as the Swift package.

## Initialize

Call `Clarity.initialize` once at launch:

```swift theme={null}
import ClaritySDK

@main
struct MyApp: App {
    init() {
        Clarity.initialize(config: ClarityConfig(
            organizationId: "YOUR_ORGANIZATION_ID",
            integrationKey: "YOUR_INTEGRATION_KEY",
            baseUrl: "https://live-chat.leap.onclarity.com"
        ))
    }
    // ...
}
```

In a UIKit app, do the same in `application(_:didFinishLaunchingWithOptions:)`.

### Configuration options

| `ClarityConfig` parameter | Default | Description |
| - | - | - |
| `presentationMode` | `.bottomSheet` | Default presentation for `show()`. `.fullScreen` is the alternative. |
| `sheetHeightFraction` | `0.85` | Share of the screen the bottom sheet covers, clamped to 0.3–1.0. |
| `autoFocusInput` | `false` | Focus the message box when the customer enters a conversation. |
| `autoPreload` | `true` | Warm the chat in the background after launch so the first open is instant. |
| `language` | Device language | UI language of the chat, for example `"en"` or `"ar"`. |
| `appId` | Your bundle identifier | Must match the channel's App ID. |
| `showNativeCloseButton` | `false` | Draw an extra native close button. The chat has its own. |
| `loadTimeout` | `15` seconds | How long to wait for the chat to load before reporting a failure. |
| `showDiagnosticsOnFailure` | Debug builds only | Show a developer screen naming the reason when the chat fails to open. |

## Identify the user

After your user logs in, and before the first open:

```swift theme={null}
Clarity.identify(ClarityUser(
    externalId: user.id,          // stable id in your system
    firstName: user.firstName,
    lastName: user.lastName,
    email: user.email
))
```

`ClarityUser` also takes `phone` and `avatar`.

* Calling `identify` with a **different** `externalId` is a logout plus login: the previous user's session is wiped, so conversations never leak between users.
* Calling it again with the **same** `externalId` and new profile fields updates the profile and keeps the conversation.
* On logout, call `Clarity.resetSession()`.

### Verify the user (signed identity)

> [!NOTE]
>
> `ClarityUser.jwt` is not in ClaritySDK 1.3.0. It ships in the next release; this section describes it so you can prepare your backend.

An `externalId` alone is a claim the app makes about itself. To have Clarity trust it, your backend signs a short-lived JWT for the user with the channel's identity secret, and the app passes it as `jwt`:

```swift theme={null}
let token = try await myBackend.fetchClarityToken()   // your own authenticated endpoint

Clarity.identify(ClarityUser(
    externalId: user.id,
    email: user.email,
    jwt: token
))
```

The secret is in the iOS integration's dialog under **Identity verification**. The token format, the claims and the **Require a verified identity** switch are the same as for the web widget: see [Verify the visitor](/sdks/web-widget.md#verify-the-visitor-signed-identity).

* When the token expires, fetch a new one and call `identify` again with the same `externalId`. The conversation is kept.
* A refused token is reported through `onError` as `.pageLoadFailed` with code `401`, and the message names the reason.

> [!WARNING]
>
> Never put the identity secret in the app. Anyone who extracts it from the binary can impersonate any of your users. `ClarityUser.userHash` is deprecated and was never verified.

## Open the chat

```swift theme={null}
Clarity.show()                                                // config's default presentation
Clarity.show(mode: .bottomSheet, sheetHeightFraction: 0.85)
Clarity.show(mode: .fullScreen)
Clarity.dismiss()
```

The chat appears only once it has loaded and painted. If it cannot load, the failure arrives through your listener instead of a blank sheet. In an app with several scenes, `Clarity.show(mode:sheetHeightFraction:from:)` presents from a view controller you choose.

## Badge your Help button

Every `ClarityListener` method is optional:

```swift theme={null}
final class SupportBridge: ClarityListener {
    func onNewMessage(_ textPreview: String) {
        // an agent replied while the chat was closed
    }
    func onUnreadCountChanged(_ count: Int) {
        helpBadge.isHidden = count == 0   // 1 = unseen replies, 0 = none
    }
    func onTicketStatusChanged(_ status: String) {
        // "open" or "closed"
    }
    func onError(_ error: ClarityError) {
        if !error.recoverable { /* hide the Help entry or fall back to email */ }
    }
}

Clarity.setListener(bridge)   // the SDK keeps a strong reference
```

You can also read `Clarity.unreadCount`, `Clarity.isWidgetVisible` and `Clarity.currentTicketStatus` directly. `unreadCount` is a flag: `1` while unseen replies exist, else `0`.

Delivery while the chat is closed is best effort. When freshness matters, for example when the app returns to the foreground, call `Clarity.refresh()`. Missed replies and status changes arrive through the same listener.

## Attributes and product events

```swift theme={null}
// On the contact, long-lived. String, Int, Double and Bool keep their type;
// [String] selects option ids of a list attribute; nil removes the attribute.
Clarity.setUserAttributes(["plan": "gold", "seat_count": 7, "vip": true])

// On the next ticket the customer opens, and only that one:
Clarity.setTicketAttributes(["order_id": "A-1042"])

// Product events, for automations and AI agent context:
Clarity.logEvent("checkout_failed", params: ["code": "card_declined"])
```

These calls are safe from any thread at any time; values queue until the chat session is live. Product events are processed once product events are enabled for your organization; ask your Clarity contact.

> [!NOTE]
>
> If your workspace marks contact attributes as required, set them right after `Clarity.initialize`, before the chat first loads. A session missing a required attribute is refused, and `show()` reports `.pageLoadFailed` naming the missing keys. With `autoPreload` on, the first load starts about 1.5 seconds after launch; alternatively set `autoPreload: false` and call `Clarity.preload()` once the attributes are set.

## Notifications

### While the app is running

When a reply arrives with the chat closed, the SDK raises `onNewMessage`, sets the unread flag and posts a local notification, but only if your app already has notification permission. The SDK never prompts. Route taps back to the chat from your `UNUserNotificationCenterDelegate`:

```swift theme={null}
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    if Clarity.handleNotificationResponse(response) {   // reopens the chat
        completionHandler()
        return
    }
    // your own notifications
}
```

To show banners while your app is in the foreground, your `willPresent` delegate must also allow them, for example `completionHandler([.banner, .sound])`.

### Push notifications (APNs)

Forward your device token so replies can reach the device while the app is in the background:

```swift theme={null}
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Clarity.setDeviceToken(deviceToken)
}

func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    if Clarity.isClarityPush(userInfo: userInfo) {
        _ = Clarity.handlePush(userInfo: userInfo)
        completionHandler(.newData)
        return
    }
    // your own pushes
}
```

`setDeviceToken` also accepts the token as a hex string.

> [!NOTE]
>
> Background push delivery is enabled per organization. Ask your Clarity contact about availability.

## Attachments, camera and microphone

Customers can attach images and PDFs. **Photo Library** and **Choose File** need no permissions. **Take Photo** uses the camera inside your app, and iOS terminates an app that opens the camera without a usage description. Add one to enable it:

```xml theme={null}
<key>NSCameraUsageDescription</key>
<string>The camera lets you take a photo to attach to your support conversation.</string>
```

If your organization uses voice calling or dictation in the chat, add a microphone usage description too:

```xml theme={null}
<key>NSMicrophoneUsageDescription</key>
<string>The microphone lets you talk to a voice agent or dictate a message in your support conversation.</string>
```

Without these keys the SDK disables the camera option and hides the microphone button rather than letting iOS close your app.

## Customization

The chat's colours, logos, font and texts come from **Settings > Widget** in the cabinet, the same settings as the web widget. Per-app switches such as voice calling, dictation and the Inbox tab are in the iOS integration's dialog. In code you control the presentation (`presentationMode`, `sheetHeightFraction`), `language` and `autoFocusInput`.

## API reference

| Call | Purpose |
| - | - |
| `Clarity.initialize(config:)` | Once, at launch |
| `Clarity.updateConfig(_:)` / `Clarity.config` | Switch or read the configuration at runtime |
| `Clarity.identify(_:)` / `resetSession()` | Bind your user / logout |
| `Clarity.show(mode:sheetHeightFraction:)` / `dismiss()` | Open and close |
| `Clarity.show(mode:sheetHeightFraction:from:)` | Present from a chosen view controller |
| `Clarity.preload()` | Manual warm-up when `autoPreload` is off |
| `Clarity.refresh()` | Fetch the latest ticket state |
| `Clarity.setUserAttributes` / `setTicketAttributes` / `logEvent` | Context |
| `Clarity.setDeviceToken(_:)` | Forward the APNs token (`Data` or `String`) |
| `Clarity.isClarityPush` / `handlePush` / `handleNotificationResponse` | Route pushes and notification taps |
| `Clarity.setListener(_:)` / `unreadCount` | Callbacks and unread flag |
| `Clarity.isWidgetVisible` / `currentTicketStatus` | Current state |
| `Clarity.previewWidgetUrl(...)` | Debugging: the URL the SDK would load |

## Troubleshooting

Every failure arrives in `onError` as a `ClarityError` with a `kind`:

| Kind | Meaning | Recoverable |
| - | - | - |
| `.network` | Offline, host unreachable or a server error | Yes; the next `show()` retries |
| `.handshakeRejected` | Wrong organization id, integration key or App ID | **No**; fix the configuration |
| `.pageLoadFailed` | The chat loaded but never became ready, a required attribute is missing, or the identity token was refused | Yes; the next `show()` reloads |
| `.rendererCrashed` | The WebKit renderer died | Yes; recovers on its own |
| `.internalError` | Unexpected failure caught by the SDK | Yes |

> **Nothing appears on show()**
>
> Check `onError`. `.handshakeRejected` means the organization id, integration key or App ID is wrong; the App ID must equal your bundle identifier. `.network` means the device cannot reach `baseUrl`. In debug builds, a diagnostic screen names the reason.

> **Replies while the chat is closed do not notify**
>
> Local notifications need your app to have asked for notification permission. The SDK never prompts.

> **Take Photo does nothing**
>
> Add `NSCameraUsageDescription` to your Info.plist.

> **Which URL is the SDK loading?**
>
> `Clarity.previewWidgetUrl(...)` returns it, and `onDebugEvent` on your listener traces each step: the session handshake, navigation and the chat becoming ready.


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