Kindly iOS SDK — Deep Links & Link Interception Reference
Deep reference for two related but distinct features. Companion to 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:
kindly://chat → opens the chat UI
kindly://chat/dialogue/{dialogueId} → opens chat and triggers the named dialogue1. Register the URL scheme
In Info.plist (or via Xcode → Target → Info → URL Types):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>kindly</string>
</array>
</dict>
</array>2. Hand incoming URLs to the SDK
SwiftUI / SceneDelegate
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
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if KindlySDK.handleURL(url) { return true }
return false
}SwiftUI App
@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:
KindlySDK.triggerDialogue(id: "welcome_flow")Requires an active connection (check KindlySDK.state first if unsure — see EVENTS.md).
handleURL(_:) reference
@discardableResult
public class func handleURL(_ url: URL) -> BoolReturns | 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
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
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 = handlerKindlySDK.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.
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:
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.