---
title: Kindly iOS SDK — Deep Links & Link Interception Reference
slug: sdk/ios-sdk-skill-deeplinks
description: Complete reference for deep links and link interception in the Kindly iOS SDK. Use when handling kindly:// URLs from outside the app, opening specific dialogues from a deep link, or implementing KindlyChatClientDelegate to intercept links and button…
createdAt: 2026-11-04T11:40:17.267Z
---

Deep reference for two related but distinct features. Companion to [SKILL.md](https://docs.kindly.ai/sdk/ios-sdk-skill.md).

| Feature               | Direction         | Mechanism                                                   |
| --------------------- | ----------------- | ----------------------------------------------------------- |
| **Deep links**        | External → Kindly | `kindly://` URL scheme handled by `KindlySDK.handleURL(_:)` |
| **Link interception** | Kindly → External | `KindlyChatClientDelegate.shouldHandleLink(url:)`           |

***

## Deep Links: `kindly://` URL scheme

The SDK accepts URLs of the form:

```javascript
kindly://chat                            → opens the chat UI
kindly://chat/dialogue/{dialogueId}      → opens chat and triggers the named dialogue
```

### 1. Register the URL scheme

In `Info.plist` (or via Xcode → Target → Info → URL Types):

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>kindly</string>
        </array>
    </dict>
</array>
```

### 2. Hand incoming URLs to the SDK

### SwiftUI / SceneDelegate

```swift
func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
    for ctx in contexts {
        if KindlySDK.handleURL(ctx.url) { return }
        // not a Kindly URL — handle elsewhere
    }
}
```

### AppDelegate

```swift
func application(_ app: UIApplication,
                 open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    if KindlySDK.handleURL(url) { return true }
    return false
}
```

### SwiftUI App

```swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    _ = KindlySDK.handleURL(url)
                }
        }
    }
}
```

### 3. Programmatic dialogue trigger (no URL needed)

If the trigger is internal (button in your own UI), skip the URL round-trip:

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

Requires an active connection (check `KindlySDK.state` first if unsure — see [EVENTS.md](https://docs.kindly.ai/sdk/ios-sdk-skill-events.md)).

### `handleURL(_:)` reference

```swift
@discardableResult
public class func handleURL(_ url: URL) -> Bool
```

| Returns | Meaning                                                                                              |
| ------- | ---------------------------------------------------------------------------------------------------- |
| `true`  | The URL had scheme `kindly` and was processed (chat may have been opened, dialogue triggered, etc.). |
| `false` | The URL was not a Kindly URL. Forward it to the rest of your app's URL handling.                     |

***

## Link Interception: `KindlyChatClientDelegate`

When a user taps a link **inside** the chat (in a bot message, button URL, etc.), Kindly normally opens it in the system browser. Implement `KindlyChatClientDelegate` to intercept links and button taps.

### Protocol definition

```swift
public protocol KindlyChatClientDelegate: NSObject {
    /// Called when the user taps a chat button.
    func didPressButton(chatButton: ExternalChatButton, chatLog: [ExternalChatMessage])

    /// Called before opening a link from the chat.
    /// Return `true` to let Kindly open it (default).
    /// Return `false` to handle it yourself; Kindly will not open the link.
    func shouldHandleLink(url: URL) -> Bool
}

public extension KindlyChatClientDelegate {
    func shouldHandleLink(url: URL) -> Bool { return true }
}
```

`shouldHandleLink` has a default implementation that returns `true`, so you only need to implement it if you want to override.

### Wiring it up

```swift
final class ChatLinkHandler: NSObject, KindlyChatClientDelegate {
    func didPressButton(chatButton: ExternalChatButton, chatLog: [ExternalChatMessage]) {
        print("User tapped: \(chatButton.label)")
    }

    func shouldHandleLink(url: URL) -> Bool {
        // Open in-app instead of Safari for our own domain
        if url.host == "example.com" {
            UIApplication.shared.open(url)
            return false
        }
        return true
    }
}

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

`KindlySDK.delegate` is held weakly internally — keep a strong reference (e.g., a property) or it will be deallocated.

***

## Combined Pattern: deep links + interception

A common setup: external `kindly://` deep links open chat, *and* in-chat links route through your nav stack instead of Safari.

```swift
final class KindlyIntegration: NSObject, KindlyChatClientDelegate {
    static let shared = KindlyIntegration()

    func boot() {
        KindlySDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET")
        KindlySDK.delegate = self
    }

    func handle(externalURL url: URL) -> Bool {
        return KindlySDK.handleURL(url)
    }

    func didPressButton(chatButton: ExternalChatButton, chatLog: [ExternalChatMessage]) {
        Analytics.track("kindly_button_tap", value: chatButton.label)
    }

    func shouldHandleLink(url: URL) -> Bool {
        if url.host == "example.com" {
            AppRouter.shared.open(url)
            return false
        }
        return true
    }
}
```

***

## Receiving Button-Click Details via Events

The delegate's `didPressButton` gives you the legacy `ExternalChatButton` shape. For richer context (button type, exchange info, full chat log) use the `kindly:buttonclick` event instead — see [EVENTS.md](https://docs.kindly.ai/sdk/ios-sdk-skill-events.md):

```swift
KindlySDK.events
    .sink { event in
        if case .buttonClick(let btn, let type, _, _) = event.detail {
            print("Tapped [\(type)] \(btn.label)")
        }
    }
    .store(in: &cancellables)
```

Both paths fire for the same tap; pick whichever fits your existing wiring.

***

## Notes & Gotchas

- `handleURL(_:)` returns `false` if `start()` hasn't been called — the SDK can't open chat until it's initialized. Either start the SDK before forwarding URLs, or queue the URL and replay after `start()`.
- `kindly://chat/dialogue/{id}` requires the dialogue ID to exist in the bot configuration. Unknown IDs are logged as warnings; chat still opens but no dialogue is triggered.
- The system browser opens in `SFSafariViewController` by default for URLs the SDK handles — your `shouldHandleLink` return value short-circuits that.
- If you only want to intercept the URL but still let Kindly close the keyboard / log the event, you can do work in `shouldHandleLink` and still return `true` (Kindly will then also open the URL — usually you'll want to return `false` after taking over).
- The delegate is set per-process; only one delegate is active at a time. Setting a new one replaces the previous.
