---
title: Kindly Web View SDK, Links Reference
slug: sdk/web-view-sdk-skill-links
description: Reference for links in the Kindly web view SDK on iOS, Android, Flutter and React Native. Use when deciding what happens when a user taps a link in the chat, setting KindlyWebViewSDK.delegate, KindlyWebViewSDK.interface or setDelegate(shouldHandleLink)…
createdAt: 2026-11-04T11:39:34.834Z
---

What happens to a link in the chat, and how the app takes over the ones it wants, on all four platforms. Companion to [SKILL.md](https://docs.kindly.ai/sdk/web-view-sdk-skill.md). The guide page is [Link Interception](https://docs.kindly.ai/sdk/web-view-sdk-link-interception)

***

## What the SDK does with a link

No link ever opens inside the chat. When the user taps a link, in a message, on a button or one that would open a new window, the SDK stops the web view's navigation and then:

| Scheme                                         | What happens                                             |
| ---------------------------------------------- | -------------------------------------------------------- |
| `http`, `https`                                | Opens in the browser, or in the app that claims the link |
| `mailto`                                       | Opens the mail app                                       |
| `tel`                                          | Opens the phone app                                      |
| `sms`                                          | Opens the messages app                                   |
| Anything else, such as `myapp://` or `intent:` | Blocked. Nothing opens and the delegate is not asked     |

On iOS the SDK opens the link with `UIApplication.shared.open`, so the app needs no `LSApplicationQueriesSchemes`. On Android it starts an `ACTION_VIEW` intent in the `BROWSABLE` category, without Custom Tabs. When no app on the device can open it, nothing happens and the SDK logs a warning

Before anything opens, three rules apply, and a link that breaks one never reaches the delegate:

- Links open only while the chat is displayed
- The same link opens at most once in 1.5 seconds, so a double tap opens it once
- At most three links open in 10 seconds. A refused link does not count toward that

***

## The delegate

The delegate is asked about every link that passed the rules above. Return `true` to let the SDK open it, or `false` to handle it in the app

### iOS

```swift
import KindlyWebView

final class ChatLinks: KindlyWebViewClientDelegate {
    func shouldHandleLink(url: URL) async -> Bool {
        guard url.host == "myapp.example" else { return true }
        await MainActor.run {
            // open the matching screen in the app
        }
        return false
    }
}

let chatLinks = ChatLinks()
KindlyWebViewSDK.delegate = chatLinks
```

- `KindlyWebViewClientDelegate` has one method, `shouldHandleLink(url:) async -> Bool`, and a synchronous implementation satisfies it too
- The SDK holds the delegate weakly, so keep a reference to it, as `chatLinks` above
- The method is not isolated to the main actor, so do UI work on the main actor, as above

### Android

```kotlin
import ai.kindly.webview.KindlyWebViewInteraction
import ai.kindly.webview.KindlyWebViewSDK

KindlyWebViewSDK.`interface` = object : KindlyWebViewInteraction {
    override suspend fun shouldHandleLink(url: String): Boolean {
        if (!url.startsWith("https://myapp.example/")) return true
        openInApp(url)
        return false
    }
}
```

- `KindlyWebViewInteraction` has one method, `suspend fun shouldHandleLink(url: String): Boolean`, which defaults to `true`
- The SDK calls it on the main thread, so suspend for slow work instead of blocking
- An exception counts as `true`
- Set `interface` to `null` to remove the delegate

### Flutter

```dart
await KindlyWebViewSDK.setDelegate(shouldHandleLink: (String url) async {
  if (url.startsWith('https://myapp.example/')) {
    openInApp(url);
    return false; // handled by the app
  }
  return true; // the SDK opens it
});
```

- The callback is a `KindlyWebViewLinkCallback`, `FutureOr<bool> Function(String url)`
- An answer that throws counts as `true`
- `clearDelegate()` removes it, and leaves a delegate the app set natively alone
- The callback reaches the Flutter engine whose Dart code last called `start`. After a hot restart, call `start` and `setDelegate` again. When that engine goes away, links open outside the app until another engine calls `start`

### React Native

```typescript
KindlyWebViewSDK.setDelegate({
  shouldHandleLink: async (url) => {
    if (url.startsWith('https://myapp.example/')) {
      openInApp(url);
      return false; // handled by the app
    }
    return true; // the SDK opens it
  },
});
```

- Only an explicit `false` keeps the link in the app, and an answer that throws counts as `true`
- `setDelegate(null)` removes the delegate set here and leaves one the app set natively alone
- After a JavaScript reload, call `setDelegate` again. Until then links open outside the app

`openInApp` stands for the app's own navigation

On every platform the delegate stays set across `start` and `kill`

***

## Timeouts

| Who waits                        | Limit                                               | No answer in time means        |
| -------------------------------- | --------------------------------------------------- | ------------------------------ |
| The native SDK, for the delegate | 5 seconds                                           | `true`: the SDK opens the link |
| Flutter, for the callback        | 4 seconds, `KindlyWebViewSDK.linkCallbackTimeout`   | `true`                         |
| React Native, for the callback   | 4 seconds, `KindlyWebViewSDK.linkCallbackTimeoutMs` | `true`                         |

The wrappers answer before the native SDK gives up. On Android, React Native pauses JavaScript timers while the chat screen is in front, so the package's own 4 seconds do not fire there and the native 5 seconds apply. A delegate that answers late never holds the chat, because the navigation was already stopped

***

## Routing a bot link into the app

A custom scheme such as `myapp://orders/42` is blocked before the delegate is asked. To send the user from the chat to a screen in the app:

1. Put an `https` link on a domain the app owns in the bot's answer or button, for example `https://myapp.example/orders/42`
2. In the delegate, match that domain, open the screen and return `false`
3. Return `true` for every other link

The chat stays open when the delegate returns `false`. To show the app's screen in its place, close the chat in the delegate:

```swift
KindlyWebViewSDK.closeChat()
```

```kotlin
KindlyWebViewSDK.close()
```

```dart
await KindlyWebViewSDK.closeChat();
```

```typescript
await KindlyWebViewSDK.closeChat();
```

***

## Debugging

| Symptom                                                          | Likely cause                                                                                                                                  |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| A `myapp://` link does nothing                                   | Custom schemes are blocked. Link to `https` on the app's domain and intercept it                                                              |
| The delegate is never asked                                      | On iOS the delegate was released: keep a reference. In Flutter or React Native, `setDelegate` has not run again after a hot restart or reload |
| The link opens the browser although the delegate returns `false` | The answer took longer than the timeout, or threw                                                                                             |
| Nothing opens on Android                                         | No app on the device handles the link. The log says so under the tag `KindlyWebView`                                                          |
| A second tap does nothing                                        | The same link opens once in 1.5 seconds, and at most three links in 10 seconds                                                                |
| A link opens nothing while the chat is hidden                    | Links open only while the chat is displayed                                                                                                   |

With debug logs on, the SDK logs each link's scheme and outcome, never the URL beyond its scheme and host

***

## Security notes

- The URL comes from the chat page. Check the host and the path before acting on it, and never treat it as an instruction the app trusts
- Return `false` only for links the app handles. A delegate that returns `false` for everything silently swallows every link in the chat
