---
title: Kindly iOS SDK
slug: sdk/ios-sdk-skill
description: Full Kindly Chat SDK setup for iOS. Use when the user asks to add Kindly, add customer support chat, add a chat widget, integrate KindlySDK, or mentions kindly-ai, sdk-chat-ios, or KindlyChatClient in a Swift/UIKit/SwiftUI app. Covers install via Swift…
createdAt: 2026-11-04T11:40:19.871Z
---

Integrate the Kindly Chat SDK into an iOS app — a fully-featured UIKit-based chat widget with real-time messaging, push notifications, custom theming, authentication, deep links, and an event stream.

## Invoke This Skill When

- User asks to "add Kindly", "add chat support", "add customer support chat", or "add a chat widget" to an iOS app
- User mentions `KindlySDK`, `KindlyChatClient`, `kindly-ai`, `sdk-chat-ios`, or the iOS Kindly SDK
- User wants to launch an in-app chat bot, connect to a Kindly workspace via bot key, or integrate handover-to-human support

:::hint{type="info"}
**Note:** This skill reflects the public API as of iOS SDK `2.1.x`. Always verify against the [iOS SDK docs](https://docs.kindly.ai/sdk/ios-sdk) if behavior differs.
:::

***

## Phase 1: Detect

Run these commands to understand the project before making changes:

```bash
grep -Ei "kindly|KindlySDK|KindlyChatClient" Package.swift Podfile Cartfile 2>/dev/null

grep -rE "@main|struct.*App.*:.*App" --include="*.swift" . 2>/dev/null | head -5
grep -rE "AppDelegate|UIApplicationMain" --include="*.swift" . 2>/dev/null | head -5

grep -E "platforms:|\.iOS" Package.swift 2>/dev/null
grep -E "platform :ios" Podfile 2>/dev/null

grep -rE "UNUserNotificationCenter|registerForRemoteNotifications" --include="*.swift" . 2>/dev/null | head -3

grep -rE "didReceiveRegistrationToken|Messaging\\.messaging" --include="*.swift" . 2>/dev/null | head -3
```

**What to note:**

| Question                                          | Why it matters                                       |
| ------------------------------------------------- | ---------------------------------------------------- |
| `Kindly` already in `Package.swift` or `Podfile`? | If yes, skip install and jump to initialization.     |
| SwiftUI (`@main App`) or UIKit (`AppDelegate`)?   | Determines where `KindlySDK.start(...)` goes.        |
| Deployment target?                                | Kindly requires **iOS 15.0+**.                       |
| Push notifications already wired?                 | Decides whether we set up APNS delegate integration. |

***

## Phase 2: Install

Kindly is distributed as an XCFramework via Swift Package Manager or CocoaPods. It has no third-party dependencies: it reports its own errors to Kindly (opt-out available) without bundling or requiring the Sentry SDK, so it works alongside your app's own Sentry.

### Option A — Swift Package Manager (recommended)

In Xcode: **File → Add Package Dependencies…** and enter:

```javascript
https://github.com/kindly-ai/sdk-chat-ios
```

Select the latest major (`2.x`). Add the `KindlySDK` product to your app target.

Or in `Package.swift`:

```swift
dependencies: [
    .package(url: "https://github.com/kindly-ai/sdk-chat-ios", from: "3.0.14"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "KindlySDK", package: "sdk-chat-ios"),
        ]
    ),
],
```

### Option B — CocoaPods

```ruby
platform :ios, '15.0'
use_frameworks!

target 'YourApp' do
  pod 'Kindly', '~> 2.1'
end
```

Then:

```bash
pod install
```

:::hint{type="info"}
**Build issue (Xcode 14+):** If you see `rsync.samba` sandbox errors, go to Target Settings → *Build Settings* → **Enable User Script Sandbox** → `NO`.
:::

***

## Phase 3: Initialize

Call `KindlySDK.start(botKey:market:)` once at app launch. The `market` slug is **required** (see [Markets](docId:8GUlaVvd5wCu7J4PujxTf)). It's **idempotent and self-healing** — safe to call multiple times with the same bot key, and automatically tears down and re-connects if the bot key changes.

### UIKit — `AppDelegate`

```swift
import UIKit
import Kindly

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        KindlySDK.start(
            botKey: "YOUR_BOT_KEY",
            languageCode: "en",
            market: "YOUR_MARKET"   // required — see Markets
        )
        return true
    }
}
```

### SwiftUI — `App` struct

```swift
import SwiftUI
import Kindly

@main
struct MyApp: App {
    init() {
        KindlySDK.start(
            botKey: "YOUR_BOT_KEY",
            languageCode: "en",
            market: "YOUR_MARKET"   // required — see Markets
        )
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
```

:::hint{type="info"}
`start()` guards internally. Calling any other `KindlySDK.*` method before `start()` logs an error and no-ops — it never crashes.
:::

`market` is **required** — pass the slug configured for the bot (see [Markets](docId:8GUlaVvd5wCu7J4PujxTf) under Common Tasks):

```swift
KindlySDK.start(botKey: "YOUR_BOT_KEY", languageCode: "en", market: "eu")
```

***

## Phase 4: Launch the Chat

Display the chat UI from anywhere after `start()` has been called:

```swift
KindlySDK.displayChat()
```

Optionally launch straight into a specific dialogue:

```swift
KindlySDK.displayChat(triggerDialogueId: "welcome_flow")
```

Dismiss the UI without ending the session (messages are preserved):

```swift
KindlySDK.closeChat()
```

***

## Common Tasks

### Authentication

Provide a callback that supplies a fresh JWT when the SDK needs one:

```swift
KindlySDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET") { chatId, promise in
    // Fetch a JWT for chatId, then:
    promise.fulfill("YOUR_JWT_TOKEN")
    // Or on failure:
    // promise.reject(error)
}
```

Alternatively, set the callback on the config object after `start()`:

```swift
KindlySDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET")
KindlySDK.config.getAuthToken = { chatId, promise in
    promise.fulfill("YOUR_JWT_TOKEN")
}
```

Manually save a token (e.g., one you already have):

```swift
let saved: Bool = KindlySDK.saveAuthToken("YOUR_JWT_TOKEN")
```

**Identity providers that issue more than one token.** Return a `KindlyAuthCredentials` instead of a string when you have a companion access token, an issuer, or an opaque token whose expiry only you know:

```swift
KindlySDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET") { chatId, promise in
    promise.fulfill(
        KindlyAuthCredentials(
            idToken: idToken,           // optional, the OIDC id token if you get one
            accessToken: accessToken,   // optional, the access token if you get one
            issuer: "https://idp.example.com/",  // optional, sent as X-Auth-Issuer
            expiresAtUnixSeconds: expiresAt      // optional, supply it for opaque tokens
        )
    )
}
```

Both tokens are optional and at least one has to be set. `Authorization` is mandatory on Kindly's auth route, so whichever token you have becomes the bearer: `idToken` first, then `accessToken`. A provider that issues only an access token is a normal supported case, not a workaround. `X-Access-Token` is sent only when an access token accompanies a *different* id token, because Kindly checks the companion against the id token's `at_hash` and a duplicate would fail an auth that would otherwise pass.

Three read-only accessors say what the SDK will actually send, so you never re-derive the rules:

```swift
credentials.bearerToken           // idToken ?? accessToken, nil if neither is set
credentials.hasToken              // bearerToken != nil, the real "can this authenticate?" check
credentials.companionAccessToken  // what goes in X-Access-Token, or nil when nothing will
```

A single-JWT integration never needs this type. Keep returning a string.

**Refresh and expiry.** The SDK refreshes 30 seconds before the credentials expire, taking the earliest of three sources: the `expires_at` Kindly returns, the `expiresAt` you supplied, and the `exp` claim of the bearer when it is a JWT. If none of the three is known the credentials count as non-expiring and are never proactively refreshed. Correct for a JWT with no `exp`, a trap for an opaque token: there is no claim to read, so supply `expiresAt` or the SDK keeps using it until Kindly rejects it.

**Re-authenticating mid-session.** The SDK refreshes on expiry by itself. These two are for when the *identity* changes rather than the token:

```swift
KindlySDK.authenticate()     // user signed in, or switched account; re-binds in place
KindlySDK.deauthenticate()   // user signed out; chat carries on anonymously, history intact
```

`authenticate()` asks your callback for fresh credentials and binds them to the chat that is already open, socket and history intact. `deauthenticate()` invalidates the identity on the backend, then clears the stored credentials. It is a sign-out, not an `endChat()`, and it is safe to call when the chat is already anonymous.

### Markets (per-location settings)

`market` is **required** at `start()` — pass the slug configured for the bot:

```swift
KindlySDK.start(botKey: "YOUR_BOT_KEY", languageCode: "en", market: "eu")
```

The slug is whatever is configured in the Kindly platform (e.g. `eu`, `us`, `general`). The SDK loads that market's settings (`/settings/{botKey}/{market}.json`) and tells `/connect` which market the host picked, so greeting and bot responses are localized to it. `market` is independent of `languageCode`.

**No fallback:** if the requested market's settings can't be loaded, settings loading fails and the chat won't load. There is no fallback to another market and no base/per-bot settings file outside of markets. Find market slugs in the Kindly platform (Settings → General → Details & markets).

### Context (per-message metadata)

Attach key/value context to the **next** message or button click request:

```swift
KindlySDK.setNewContext([
    "userId": "12345",
    "plan": "premium"
])
```

Clear pending context:

```swift
KindlySDK.clearNewContext()
```

### Theming

Apply a custom theme — nil colors fall back to the API theme or default:

```swift
KindlySDK.setCustomTheme(
    background: .white,
    botMessageBackground: UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1),
    botMessageText: .black,
    buttonBackground: .systemBlue,
    buttonText: .white,
    navBarBackground: .systemBlue,
    navBarText: .white,
    userMessageBackground: .systemBlue,
    userMessageText: .white
)
```

Or pass a full `CustomTheme` object:

```swift
let theme = CustomTheme(background: .white, buttonBackground: .systemBlue, /* ... */)
KindlySDK.setCustomTheme(theme)
```

Revert to the API/default theme:

```swift
KindlySDK.clearCustomTheme()
```

Themes apply **live**: calling `setCustomTheme(...)` or `clearCustomTheme()` while the chat is on screen updates the page background, navigation bar, message bubbles, system messages, typing indicator, attachments, image cells, form cells, action buttons, file-upload labels, the input field, and the toolbar in place. No need to dismiss and re-display the chat. This makes it natural for host apps to swap themes in response to system appearance changes (light/dark mode, custom toggles, etc.) by calling `setCustomTheme(...)` again with the new colors.

### Change Language at Runtime

Returns a `Promise<Void>` that resolves when the new language is applied:

```swift
KindlySDK.setLanguage("fr").then {
    print("Language changed")
}.catch { error in
    print("Language change failed: \(error)")
}
```

### Trigger a Specific Dialogue

Requires an active connection:

```swift
KindlySDK.triggerDialogue(id: "welcome_flow")
```

### Send a Message Programmatically

Drive the chat from the host app — send a message **as the user** without going through the input field:

```swift
KindlySDK.sendMessage("Hi, I have a question about my order")
```

With per-call context (overrides any `setNewContext(_:)` value for this call only):

```swift
KindlySDK.sendMessage(
    "Where is my order?",
    newContext: ["orderId": "12345"]
)
```

Returns `Promise<ExternalChatMessage>` so you can observe success/failure:

```swift
KindlySDK.sendMessage("Hello").then { (message: ExternalChatMessage) in
    print("Sent — server id: \(message.id)")
}.catch { error in
    print("Send failed: \(error)")
}
```

:::hint{type="info"}
The SDK renders the bubble locally and reconciles with the server-assigned id on response. **Do not also display the message yourself.** Rejects with `KindlySDKError.chatNotConnected` if the chat is not connected, and `KindlySDKError.invalidData` for empty input.
:::

### Deep Links

Kindly handles URLs with scheme `kindly://chat/dialogue/{id}`. In your `SceneDelegate` or `AppDelegate`:

```swift
func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
    for ctx in contexts {
        if KindlySDK.handleURL(ctx.url) { return }
    }
}
```

`handleURL(_:)` returns `true` if the URL was a Kindly deep link and was handled.

### Push Notifications (APNS)

Forward your APNS token to Kindly:

```swift
func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    KindlySDK.setAPNSDeviceToken(deviceToken)
}
```

To let the SDK route Kindly notifications automatically, set its notification delegate:

```swift
UNUserNotificationCenter.current().delegate = KindlySDK.notificationDelegate
```

Or handle notifications manually:

```swift
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    if KindlySDK.isKindlyNotification(response.notification.request.content.userInfo) {
        KindlySDK.notificationResponseReceived(response)
    }
    completionHandler()
}
```

**Notification Service Extension (recommended, SDK 3.0.12+).** Add a Notification Service Extension target whose class is `final class NotificationService: KindlyNotificationServiceExtension {}`, link `Kindly` into it, and give both targets the same Keychain Sharing group (your app's bundle id). That is all: the SDK detects the extension and tells Kindly this app can take visible pushes, so they become visible, the extension decrypts them before iOS shows them, in every app state including when the app was killed, and the iOS silent-push rate limit no longer applies. `KindlySDK.notificationMode` overrides the detection. Full steps in [PUSH.md](https://docs.kindly.ai/sdk/ios-sdk-skill-push.md) Section 4.

### Link Interception (Delegate)

Intercept links tapped inside the chat (e.g., to open in-app instead of Safari):

```swift
class ChatLinkHandler: NSObject, KindlyChatClientDelegate {
    func shouldHandleLink(url: URL) -> Bool {
        // Return true to let Kindly open it. Return false to take over.
        if url.host == "example.com" {
            UIApplication.shared.open(url)
            return false
        }
        return true
    }
}

let handler = ChatLinkHandler()
KindlySDK.delegate = handler
```

### Notification Interception (Delegate)

The SDK decrypts every Kindly push before anything is shown (silent pushes in the app, visible pushes in the Notification Service Extension or, without one, in the app). The display rule in the foreground:

- **Background** → always show
- **Foreground on the chat conversation** → suppress (the user can already see the message)
- **Foreground anywhere else** (settings, language, host-app screens) → show

For every Kindly push the app process sees in plaintext the SDK calls `shouldHandleNotification(notification:)` on the delegate **before** any display gate. The callback fires regardless of app state — use it to run analytics or to take over the presentation entirely:

```swift
extension AppDelegate: KindlyChatClientDelegate {
    func shouldHandleNotification(notification: ExternalNotification) -> Bool {
        Analytics.track("kindly_notification", id: notification.id)

        // Take over with custom UI:
        if isShowingCustomInbox {
            customInbox.append(notification)
            return false   // SDK does not show its banner
        }
        return true        // let the SDK show the banner
    }
}
```

The `ExternalNotification` value has `id`, `title`, `body` (all decrypted) and `userInfo` (the original APNs dictionary, including `aps` and any extra fields your backend attached). Same polarity as `shouldHandleLink`: `true` (default) = SDK handles, `false` = host handles.

### Events & State (Combine)

Subscribe to a unified event stream (matches the web widget API):

```swift
import Combine

var cancellables = Set<AnyCancellable>()

KindlySDK.events
    .sink { event in
        switch event.detail {
        case .load(let settings):
            print("Loaded bot: \(settings.name)")
        case .message(let newMessage, _):
            print("Message: \(newMessage.text ?? "")")
        case .buttonClick(let button, _, _, _):
            print("Button clicked: \(button.label)")
        case .ui(let action):
            print("UI action: \(action.type.rawValue)")
        case .state(let state):
            print("Connected: \(state.isConnected)")
        case .globalIcon:
            break
        }
    }
    .store(in: &cancellables)
```

State-only stream:

```swift
KindlySDK.state
    .sink { state in
        print("isConnected: \(state.isConnected), isChatDisplayed: \(state.isChatDisplayed)")
    }
    .store(in: &cancellables)
```

### End a Chat Session

Disconnect and dismiss the UI, but keep the SDK initialized for re-use:

```swift
KindlySDK.endChat().then {
    print("Chat ended")
}
```

### Kill the SDK Entirely

Fully tear down — after this, `start()` must be called again before any other API:

```swift
KindlySDK.kill().then {
    print("SDK killed")
}
```

***

## Configuration Reference

### Core Public Surface

| API                                                                         | Type                                    | Purpose                                                                                                                                                     |
| --------------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KindlySDK.start(botKey:languageCode:market:authTokenCallback:)`            | `class func`                            | Initialize the SDK. Idempotent. `market` is **required** (Markets feature, no fallback).                                                                    |
| `KindlySDK.start(botKey:languageCode:market:authCredentialsCallback:)`      | `class func`                            | Same, but the callback returns `KindlyAuthCredentials` rather than a token string.                                                                          |
| `KindlySDK.displayChat(languageCode:triggerDialogueId:)`                    | `class func`                            | Show the chat UI.                                                                                                                                           |
| `KindlySDK.closeChat()`                                                     | `class func`                            | Dismiss the UI, keep the session.                                                                                                                           |
| `KindlySDK.endChat()`                                                       | `-> Promise<Void>`                      | End session, dismiss UI, keep SDK initialized.                                                                                                              |
| `KindlySDK.kill()`                                                          | `-> Promise<Void>`                      | Tear down SDK fully.                                                                                                                                        |
| `KindlySDK.setLanguage(_:)`                                                 | `-> Promise<Void>`                      | Change language at runtime.                                                                                                                                 |
| `KindlySDK.setNewContext(_:)` / `.clearNewContext()`                        | `class func`                            | Attach context to next request.                                                                                                                             |
| `KindlySDK.sendMessage(_:newContext:)`                                      | `-> Promise<ExternalChatMessage>`       | Send a message as the user, programmatically. SDK renders the bubble — host must not also display it.                                                       |
| `KindlySDK.triggerDialogue(id:)`                                            | `class func`                            | Run a specific dialogue.                                                                                                                                    |
| `KindlySDK.handleURL(_:)`                                                   | `-> Bool`                               | Handle `kindly://chat/dialogue/{id}` deep links.                                                                                                            |
| `KindlySDK.setCustomTheme(...)` / `.clearCustomTheme()`                     | `class func`                            | Theme the chat UI.                                                                                                                                          |
| `KindlySDK.saveAuthToken(_:)`                                               | `-> Bool`                               | Manually save a JWT to Keychain.                                                                                                                            |
| `KindlySDK.saveAuthCredentials(_:)`                                         | `-> Bool`                               | Manually save full `KindlyAuthCredentials` to Keychain.                                                                                                     |
| `KindlyAuthCredentials(idToken:accessToken:issuer:expiresAt:)`              | `struct`                                | Credentials for the multi-token strategies. Every field optional, at least one of the two tokens required. `expiresAtUnixSeconds:` is the convenience init. |
| `KindlyAuthCredentials.bearerToken` / `.hasToken` / `.companionAccessToken` | derived                                 | What the SDK will send: `idToken ?? accessToken` as the bearer, and `X-Access-Token` only next to a different id token.                                     |
| `KindlySDK.authenticate()`                                                  | `-> Promise<Void>`                      | Re-authenticate the live chat with fresh credentials. For a sign-in mid-session.                                                                            |
| `KindlySDK.deauthenticate()`                                                | `-> Promise<Void>`                      | Sign out of the chat without ending it.                                                                                                                     |
| `KindlySDK.setAPNSDeviceToken(_:)`                                          | `class func`                            | Register APNS token.                                                                                                                                        |
| `KindlySDK.notificationDelegate`                                            | `NotificationDelegate`                  | SDK-managed `UNUserNotificationCenter` delegate.                                                                                                            |
| `KindlySDK.isKindlyNotification(_:)`                                        | `-> Bool`                               | Detect Kindly push payloads. Works before `start()`.                                                                                                        |
| `KindlySDK.notificationWillPresent(_:completionHandler:)`                   | `class func`                            | The SDK's presentation decision for your own `willPresent`.                                                                                                 |
| `KindlySDK.openChatOnNotificationTap`                                       | `Bool`                                  | Tapping a Kindly banner opens the chat (default `true`).                                                                                                    |
| `KindlySDK.notificationMode`                                                | `KindlyNotificationMode`                | `.automatic` (default: visible pushes when the app ships a Kindly extension, silent otherwise), `.alert` or `.silent`.                                      |
| `KindlyNotificationServiceExtension`                                        | `open class`                            | Base class for the host's Notification Service Extension.                                                                                                   |
| `KindlyPushDecryption`                                                      | `enum`                                  | `isKindlyNotification`, `decrypt(userInfo:)`, `apply(_:to:)`, `isDecrypted(_:)` for hosts with their own extension.                                         |
| `KindlySDK.events`                                                          | `AnyPublisher<KindlyEvent, Never>`      | Unified event stream.                                                                                                                                       |
| `KindlySDK.state`                                                           | `AnyPublisher<KindlyEventState, Never>` | State change stream.                                                                                                                                        |
| `KindlySDK.delegate`                                                        | `KindlyChatClientDelegate?`             | Link interception and other callbacks.                                                                                                                      |

### Configurable Properties

| Property                              | Default | Purpose                                                                                       |
| ------------------------------------- | ------- | --------------------------------------------------------------------------------------------- |
| `KindlySDK.enableCrashReporting`      | `true`  | Send the SDK's own error reports to Kindly. Set to `false` to opt out.                        |
| `KindlySDK.verboseLogging`            | `false` | Verbose logs in Xcode console.                                                                |
| `KindlySDK.config.getAuthToken`       | `nil`   | Alternative way to set `authTokenCallback`.                                                   |
| `KindlySDK.config.getAuthCredentials` | `nil`   | Alternative way to set `authCredentialsCallback`. Wins over `getAuthToken` when both are set. |

### Event Types

`KindlyEventType` (rawValue shown):

- `load` → `"kindly:load"` — settings/configuration loaded
- `message` → `"kindly:message"` — message sent or received
- `buttonClick` → `"kindly:buttonclick"` — user tapped a button
- `ui` → `"kindly:ui"` — UI action (open/close/endChat/changeLanguage/…)
- `globalIcon` → `"kindly:globalicon"` — defined but not currently emitted on mobile
- `state` → `"kindly:state"` — connection/display state changed

### Deep Link Format

Scheme: `kindly://chat/dialogue/{dialogueId}`

Passing such a URL to `KindlySDK.handleURL(_:)` either triggers the dialogue (if already connected) or opens the chat and triggers it.

***

## Verification

After wiring everything up, sanity-check the integration:

```swift
KindlySDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET")

KindlySDK.state
    .sink { state in
        print("isStarted: \(state.isStarted), isSocketConnected: \(state.isSocketConnected)")
    }
    .store(in: &cancellables)

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    KindlySDK.displayChat()
}
```

**Expected:**

- `isStarted` becomes `true` within \~1s.
- `isSocketConnected` becomes `true` shortly after.
- Chat UI appears when `displayChat()` is called.
- Sending a message produces a `.message` event in the stream.

If events don't flow:

1. Enable `KindlySDK.verboseLogging = true` and check the Xcode console.
2. Confirm the bot key is correct (wrong keys fail silently on the server).
3. Confirm the device has network access.

***

## Troubleshooting

| Issue                                              | Fix                                                                                                                                                                                                                                             |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"called before start()"` logs                     | Ensure `KindlySDK.start(botKey:)` runs **before** any other `KindlySDK.*` call.                                                                                                                                                                 |
| Chat UI doesn't appear                             | `start()` must have completed and the app must have a key window; call `displayChat()` from the main thread.                                                                                                                                    |
| Messages don't send                                | Check `KindlySDK.state` for `isSocketConnected == true`. Network issues, wrong bot key, or auth failures will keep it `false`.                                                                                                                  |
| Re-initialization with same bot key does nothing   | That's intentional — `start()` is idempotent when already connected to the same bot. Call `kill()` first if you need a full reset.                                                                                                              |
| Bot key switch drops messages                      | When the bot key changes, the SDK disconnects and reconnects. Handle `.state` events to know when the new session is ready.                                                                                                                     |
| Turning off the SDK's error reports                | `enableCrashReporting` defaults to `true`. Set `KindlySDK.enableCrashReporting = false` to disable. They go to Kindly and never reach your own Sentry project.                                                                                  |
| Links from chat open in Safari instead of your app | Implement `KindlyChatClientDelegate.shouldHandleLink(url:)` and assign to `KindlySDK.delegate`. Return `false` to take over.                                                                                                                    |
| Push notifications not arriving                    | Verify APNS token is forwarded via `setAPNSDeviceToken(_:)`; confirm `UNUserNotificationCenter.current().delegate` is set (either to `KindlySDK.notificationDelegate` or to your own delegate that calls `KindlySDK.notificationReceived(_:)`). |
| Deep link doesn't open chat                        | Ensure `KindlySDK.handleURL(_:)` is called from your `SceneDelegate`/`AppDelegate` URL handler. Scheme must be exactly `kindly://chat/dialogue/{id}`.                                                                                           |
| Custom theme only partially applied                | Only non-nil colors override; nil colors inherit. Use the `CustomTheme` init with explicit nil for every slot you don't want to override.                                                                                                       |
| WebView chat not available                         | WebView behavior is temporarily disabled across the SDK to comply with App Store policies around JS injection. Only `.native` behavior is supported.                                                                                            |
| `endChat()` promise hangs                          | A 2-second internal timeout force-resolves it even if disconnect stalls — check for reject handlers if you rely on errors.                                                                                                                      |

***

## Deep Reference

When a task needs deeper detail than this file covers, fetch the matching companion. Each is a self-contained reference designed to be loaded only when relevant:

| Topic                                                                                                                                                          | Fetch with                                                                                     |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Full event payload shapes (every `KindlyEventDetail` case + supporting types)                                                                                  | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/ios-sdk-skill-events.md`    |
| Every theme color slot, light/dark patterns, full `Theme` protocol reference                                                                                   | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/ios-sdk-skill-theming.md`   |
| `AuthTokenCallback` and `KindlyAuthCredentials` deep dive, dual-token and opaque-token setups, refresh and expiry, manual token save, end-to-end auth patterns | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/ios-sdk-skill-auth.md`      |
| `kindly://` URL scheme handling + `KindlyChatClientDelegate` link interception                                                                                 | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/ios-sdk-skill-deeplinks.md` |
| APNS token registration, notification routing, coexisting with other providers, Notification Service Extension setup (mutable-content + shared keychain)       | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/ios-sdk-skill-push.md`      |

## Additional Resources

- [iOS SDK guides](https://docs.kindly.ai/sdk/ios-sdk)
- [Events reference (EVENTS.md)](https://docs.kindly.ai/sdk/ios-sdk-skill-events.md)
- [Theming reference (THEMING.md)](https://docs.kindly.ai/sdk/ios-sdk-skill-theming.md)
- [Auth reference (AUTH.md)](https://docs.kindly.ai/sdk/ios-sdk-skill-auth.md)
- [Deep links reference (DEEPLINKS.md)](https://docs.kindly.ai/sdk/ios-sdk-skill-deeplinks.md)
- [Push notifications reference (PUSH.md)](https://docs.kindly.ai/sdk/ios-sdk-skill-push.md)
- [API Reference (Jazzy)](https://docs.kindly.ai/sdk/ios-sdk-api-reference)
- [GitHub repository](https://github.com/kindly-ai/sdk-chat-ios)
