---
title: Events
slug: web-view-sdk-events
createdAt: 2026-11-03T10:14:16.521Z
---

The SDK reports what happens in the chat as a stream of events, and its own state as a second stream

## Listen to events

::::Tabs
:::Tab{title="iOS"}
```swift
import Combine
import KindlyWebView

var subscriptions = Set<AnyCancellable>()

KindlyWebViewSDK.events
    .sink { event in
        if case let .ui(action) = event.detail, action.type == .closeChatBubble {
            // the user minimised the chat
        }
    }
    .store(in: &subscriptions)
```

Events arrive on the main thread. Listening lasts as long as you keep the `AnyCancellable`
:::

:::Tab{title="Android"}
```kotlin
import ai.kindly.webview.models.KindlyEventDetail
import ai.kindly.webview.models.KindlyUIActionType

lifecycleScope.launch {
    KindlyWebViewSDK.events.collect { event ->
        val detail = event.detail
        if (detail is KindlyEventDetail.UI && detail.action.type == KindlyUIActionType.CLOSE_CHAT_BUBBLE) {
            // the user minimised the chat
        }
    }
}
```

`events` is a `Flow`. Listening lasts until the collecting coroutine is cancelled
:::

:::Tab{title="Flutter"}
```dart
final subscription = KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
  if (event.uiAction == KindlyWebViewUiAction.closeChatBubble) {
    // the user minimised the chat
  }
});

// later
await subscription.cancel();
```

`events` is a broadcast stream, so any number of listeners can share it
:::

:::Tab{title="React Native"}
```typescript
const subscription = KindlyWebViewSDK.addEventListener((event) => {
  if (event.type === 'ui' && event.uiAction === 'close-chatbubble') {
    // the user minimised the chat
  }
});

// later
subscription.remove();
```
:::
::::

Events are not replayed: a listener sees only the events emitted after it started listening. Listening can start before `start`, so a listener added at launch sees the first `load` event, which comes when the chat page first loads

## Event types

| `rawType`            | iOS, Flutter and React Native | Android        | Emitted when                                                         |
| -------------------- | ----------------------------- | -------------- | -------------------------------------------------------------------- |
| `kindly:load`        | `load`                        | `LOAD`         | The widget loaded the bot's settings                                 |
| `kindly:message`     | `message`                     | `MESSAGE`      | A message was added to the conversation                              |
| `kindly:buttonclick` | `buttonClick`                 | `BUTTON_CLICK` | The user clicked a message button                                    |
| `kindly:ui`          | `ui`                          | `UI`           | A UI action happened, see [UI actions](docId\:e5k7rv4jg1Xml35TaMd_t) |
| `kindly:globalicon`  | `globalIcon`                  | `GLOBAL_ICON`  | A global icon was clicked                                            |
| `kindly:state`       | `state`                       | `STATE`        | The SDK state changed, see [State](docId\:e5k7rv4jg1Xml35TaMd_t)     |

The chat hides the widget's global icons, so `globalIcon` events practically never arrive. Flutter and React Native report a type they do not know as `unknown` and keep its name in `rawType`

## Message events

The chat emits a `message` event only while it is open, and not for the messages already there when it first renders: restored history, or the reply to a dialogue triggered as the chat opens, which on a new chat page often lands before that first render. Messages that arrive while the chat is hidden produce no event either. The next `message` event's `chatLog` carries all of them

## UI actions

| Widget value              | iOS and Flutter        | Android                   | Happens when                                    |
| ------------------------- | ---------------------- | ------------------------- | ----------------------------------------------- |
| `open-chatbubble`         | `openChatBubble`       | `OPEN_CHAT_BUBBLE`        | The chat opened                                 |
| `close-chatbubble`        | `closeChatBubble`      | `CLOSE_CHAT_BUBBLE`       | The chat was minimised or closed                |
| `end-chat`                | `endChat`              | `END_CHAT`                | The conversation ended                          |
| `restart-chat`            | `restartChat`          | `RESTART_CHAT`            | The conversation restarted                      |
| `download-chat`           | `downloadChat`         | `DOWNLOAD_CHAT`           | The user downloaded the conversation            |
| `delete-chat`             | `deleteChat`           | `DELETE_CHAT`             | The user deleted the conversation               |
| `change-language`         | `changeLanguage`       | `CHANGE_LANGUAGE`         | The user changed the chat language              |
| `show-feedback`           | `showFeedback`         | `SHOW_FEEDBACK`           | The chat showed a feedback form                 |
| `submit-feedback`         | `submitFeedback`       | `SUBMIT_FEEDBACK`         | The user submitted feedback                     |
| `dismiss-feedback`        | `dismissFeedback`      | `DISMISS_FEEDBACK`        | The user dismissed the feedback form            |
| `welcome-page-start-chat` | `welcomePageStartChat` | `WELCOME_PAGE_START_CHAT` | The user started the chat from the welcome page |

`endChat` and `kill` finish with an `end-chat` and then a `close-chatbubble` event that the SDK emits itself, with the source `programmatic`. The SDK also emits `close-chatbubble` when the chat closes without the widget reporting it. An event the SDK emits can have an empty `chatId`

::::Tabs
:::Tab{title="iOS"}
The action is `action.type`, a `KindlyUIActionType`, in the `.ui(action:)` detail. `action.source` is `user`, or `programmatic` when a call to the SDK caused it
:::

:::Tab{title="Android"}
The action is `action.type`, a `KindlyUIActionType`, in the `KindlyEventDetail.UI` detail. `action.source` is `user`, or `programmatic` when a call to the SDK caused it
:::

:::Tab{title="Flutter"}
The action is `event.uiAction`, a `KindlyWebViewUiAction`. `event.uiSource` is `user`, or `programmatic` when a call to the SDK caused it. An action this version does not know reads as `unknown`, with the widget value in `event.rawUiAction`
:::

:::Tab{title="React Native"}
The action is `event.uiAction`, the widget value as a string. `event.detail.source` is `user`, or `programmatic` when a call to the SDK caused it. An action this version does not know reads as `unknown`, with the widget value in `event.detail.rawType`
:::
::::

## What an event carries

::::Tabs
:::Tab{title="iOS"}
`KindlyEvent` has `type`, `detail`, `timestamp` (epoch milliseconds, also as a `Date` in `date`), `rawType` and `detailJSON`, the detail as JSON text. `detail` is a `KindlyEventDetail` with one case per type: `.load(settings:)`, `.message(newMessage:chatLog:)`, `.buttonClick(button:buttonType:chatLog:lastMessage:)`, `.ui(action:)`, `.globalIcon(action:)` and `.state(state:)`

The SDK's enums can gain cases in a later version, so give a `switch` over one of them a `default` or `@unknown default` case
:::

:::Tab{title="Android"}
`KindlyEvent` has `type`, `detail`, `timestamp` (epoch milliseconds), `rawType` and `detailJSON`, the detail as JSON text. `detail` is a sealed `KindlyEventDetail` with one class per type: `Load`, `Message`, `ButtonClick`, `UI`, `GlobalIcon` and `State`
:::

:::Tab{title="Flutter"}
`KindlyWebViewEvent` has `type`, `rawType`, `timestamp` (a `DateTime`), `detail` (the detail parsed into a map) and `detailJson`, the same detail as JSON text. The map's keys are listed in [the events reference](https://kindly-ai.github.io/sdk-chat-webview-sources/EVENTS.md)
:::

:::Tab{title="React Native"}
Every event has `type`, `detail`, `rawType`, `rawDetail` and `timestamp` (epoch milliseconds). `KindlyWebViewEvent` is a union, so checking `type` narrows `detail` to that type's shape. `rawDetail` is the detail as the SDK sent it, without `__proto__`, `constructor` or `prototype` keys at any depth
:::
::::

The SDK caps each detail at 4,000 characters of message text, the last 50 messages of the chat log, and 20 buttons, 20 images and 10 attachments per message. URLs in events have their query removed, and attachment download links are never included. Every value comes from the chat page, so treat it as untrusted input, and message events carry chat content, so keep them out of your logs

## State

::::Tabs
:::Tab{title="iOS"}
```swift
KindlyWebViewSDK.state
    .sink { state in
        print(state.isChatDisplayed, state.connectionState)
    }
    .store(in: &subscriptions)
```

`KindlyWebViewSDK.currentState` reads it without subscribing
:::

:::Tab{title="Android"}
```kotlin
import android.util.Log

lifecycleScope.launch {
    KindlyWebViewSDK.state.collect { state ->
        Log.d("Chat", "displayed=${state.isChatDisplayed} connection=${state.connectionState}")
    }
}
```

`KindlyWebViewSDK.currentState` reads it without collecting
:::

:::Tab{title="Flutter"}
```dart
final stateSubscription = KindlyWebViewSDK.state.listen((KindlyWebViewState state) {
  debugPrint('displayed: ${state.isChatDisplayed}, ${state.connectionState}');
});
```

`await KindlyWebViewSDK.currentState()` reads it once. A state equal to the previous one is not repeated
:::

:::Tab{title="React Native"}
```typescript
const stateSubscription = KindlyWebViewSDK.addStateListener((state) => {
  console.log(state.isChatDisplayed, state.connectionState);
});
```

`await KindlyWebViewSDK.getState()` reads it once
:::
::::

A new listener gets the current state first, then every change. The same changes also arrive as `state` events

| Field                              | Meaning                                                          |
| ---------------------------------- | ---------------------------------------------------------------- |
| `isStarted`                        | `start` was called and `kill` has not run since                  |
| `isChatDisplayed`                  | The chat screen is visible                                       |
| `isSocketConnected`, `isConnected` | Both mean the chat page is loaded, also while the chat is hidden |
| `connectionState`                  | Where the chat page is, one of the values below                  |

| `connectionState` | Meaning                                                                                                                                                                     |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `none`            | No chat page yet, before the first display or after `kill`                                                                                                                  |
| `loading`         | The chat page is loading                                                                                                                                                    |
| `connected`       | The chat page is loaded. It stays loaded while the chat is hidden                                                                                                           |
| `failed`          | The chat page failed to load, or it loaded and the chat did not open, even after one more try                                                                               |
| `unsupported`     | This device's web view cannot run the chat                                                                                                                                  |
| `disconnected`    | The chat page was released: after `endChat`, or while the chat was hidden, when memory ran low or the system ended the web view's process. The next display loads a new one |

On iOS and Android `connectionState` is a string. Flutter's `KindlyWebViewConnectionState` and React Native's string add `unknown` for a value this version does not know, with the raw value in Flutter's `rawConnectionState` and React Native's `raw.connectionState`
