---
title: Kindly Web View SDK, Events Reference
slug: sdk/web-view-sdk-skill-events
description: Reference for the Kindly web view SDK's events and state on iOS, Android, Flutter and React Native. Use when handling KindlyEvent, KindlyEventDetail, KindlyWebViewEvent, KindlyEventState or KindlyWebViewState, subscribing to KindlyWebViewSDK.events…
createdAt: 2026-11-04T11:39:35.366Z
---

Every event the SDK emits, what it carries and when it arrives, on all four platforms. Companion to [SKILL.md](https://docs.kindly.ai/sdk/web-view-sdk-skill.md). The guide page is [Events](https://docs.kindly.ai/sdk/web-view-sdk-events)

The SDK has two streams: `events`, everything that happens in the chat, and `state`, the SDK's own state. Events are not replayed: a listener sees the events emitted after it started listening, and it can start before `start`. A new state listener gets the current state first, then every change

***

## Subscribing

### iOS

```swift
import Combine
import KindlyWebView

var subscriptions = Set<AnyCancellable>()

KindlyWebViewSDK.events
    .sink { event in print(event.rawType) }
    .store(in: &subscriptions)

KindlyWebViewSDK.state
    .sink { state in print(state.connectionState) }
    .store(in: &subscriptions)

let now = KindlyWebViewSDK.currentState
```

`events` is an `AnyPublisher<KindlyEvent, Never>` and `state` an `AnyPublisher<KindlyEventState, Never>`. Both emit on the main thread

### Android

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

lifecycleScope.launch {
    KindlyWebViewSDK.events.collect { event -> Log.d("Chat", event.rawType) }
}
lifecycleScope.launch {
    KindlyWebViewSDK.state.collect { state -> Log.d("Chat", state.connectionState) }
}

val now = KindlyWebViewSDK.currentState
```

`events` is a `Flow<KindlyEvent>` and `state` a `Flow<KindlyEventState>`. The collecting block runs in the collector's coroutine context

### Flutter

```dart
final events = KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
  debugPrint(event.rawType);
});
final state = KindlyWebViewSDK.state.listen((KindlyWebViewState state) {
  debugPrint(state.rawConnectionState);
});

final KindlyWebViewState now = await KindlyWebViewSDK.currentState();

// later
await events.cancel();
await state.cancel();
```

Both are broadcast streams, so any number of listeners can share them. A state equal to the previous one is not repeated

### React Native

```typescript
const events = KindlyWebViewSDK.addEventListener((event) => console.log(event.rawType));
const state = KindlyWebViewSDK.addStateListener((s) => console.log(s.connectionState));

const now = await KindlyWebViewSDK.getState();

// later
events.remove();
state.remove();
```

A listener that throws does not stop the others

***

## Event types

| `rawType`            | iOS `KindlyEventType` | Android `KindlyEventType` | Flutter `KindlyWebViewEventType` | React Native `type` | Emitted when                                                    |
| -------------------- | --------------------- | ------------------------- | -------------------------------- | ------------------- | --------------------------------------------------------------- |
| `kindly:load`        | `load`                | `LOAD`                    | `load`                           | `load`              | The widget loaded the bot's settings                            |
| `kindly:message`     | `message`             | `MESSAGE`                 | `message`                        | `message`           | A message was added to the conversation while the chat was open |
| `kindly:buttonclick` | `buttonClick`         | `BUTTON_CLICK`            | `buttonClick`                    | `buttonClick`       | The user clicked a message button                               |
| `kindly:ui`          | `ui`                  | `UI`                      | `ui`                             | `ui`                | A UI action happened                                            |
| `kindly:globalicon`  | `globalIcon`          | `GLOBAL_ICON`             | `globalIcon`                     | `globalIcon`        | A global icon was clicked                                       |
| `kindly:state`       | `state`               | `STATE`                   | `state`                          | `state`             | The SDK state changed                                           |
| as received          |                       |                           | `unknown`                        | `unknown`           | A type this version of the package does not know                |

The chat hides the widget's global icons, so `globalIcon` events practically never arrive. `load` comes each time a chat page loads: on the first display, and again after the page was rebuilt

### When message events arrive

The chat emits a `message` event only while it is open. There is none:

- while the chat is hidden, also for a message sent with `sendMessage` or a dialogue triggered then
- for the messages already there when the chat first renders: restored history, the greeting when it lands before that render, and the reply to a dialogue triggered as the chat opens, which on a new chat page often lands before that first render

The next `message` event's `chatLog`, the last 50 messages, carries all of them. To count or read messages the user has not seen in an event, read the `chatLog` of the next one

***

## The event object

|                | iOS `KindlyEvent`                           | Android `KindlyEvent`           | Flutter `KindlyWebViewEvent`       | React Native `KindlyWebViewEvent` |
| -------------- | ------------------------------------------- | ------------------------------- | ---------------------------------- | --------------------------------- |
| Type           | `type: KindlyEventType`                     | `type: KindlyEventType`         | `type: KindlyWebViewEventType`     | `type`, a string union            |
| Raw type       | `rawType`                                   | `rawType`                       | `rawType`                          | `rawType`                         |
| Detail         | `detail: KindlyEventDetail`                 | `detail: KindlyEventDetail`     | `detail`, a `Map<String, Object?>` | `detail`, typed by `type`         |
| Detail as JSON | `detailJSON`                                | `detailJSON`                    | `detailJson`                       | `rawDetail`, the parsed object    |
| Time           | `timestamp`, epoch milliseconds, and `date` | `timestamp`, epoch milliseconds | `timestamp`, a `DateTime`          | `timestamp`, epoch milliseconds   |

iOS `KindlyEventDetail` is an enum and Android's a sealed class, with one case per type:

| Type        | iOS                                                    | Android                                                                   |
| ----------- | ------------------------------------------------------ | ------------------------------------------------------------------------- |
| load        | `.load(settings:)`                                     | `KindlyEventDetail.Load(settings)`                                        |
| message     | `.message(newMessage:chatLog:)`                        | `KindlyEventDetail.Message(newMessage, chatLog)`                          |
| buttonClick | `.buttonClick(button:buttonType:chatLog:lastMessage:)` | `KindlyEventDetail.ButtonClick(button, buttonType, chatLog, lastMessage)` |
| ui          | `.ui(action:)`                                         | `KindlyEventDetail.UI(action)`                                            |
| globalIcon  | `.globalIcon(action:)`                                 | `KindlyEventDetail.GlobalIcon(action)`                                    |
| state       | `.state(state:)`                                       | `KindlyEventDetail.State(state)`                                          |

Flutter's `detail` map and React Native's `rawDetail` hold the JSON shape listed under Payloads below. React Native's `KindlyWebViewEvent` is a union, so checking `type` narrows `detail`. Its `rawDetail` has no `__proto__`, `constructor` or `prototype` keys at any depth

`KindlyEventType`, `KindlyUIActionType` and the other Swift enums can gain cases in a later version, so give a `switch` over one of them a `default` or `@unknown default` case

***

## Payloads

The JSON keys are what `detailJSON` holds, Flutter's `detail` map and React Native's `rawDetail`. React Native's typed `detail` uses the same names unless noted

### load

| iOS and Android (`KindlyEventSettings`) | JSON        | Content                                                               |
| --------------------------------------- | ----------- | --------------------------------------------------------------------- |
| `name`                                  | `name`      | The bot's settings name                                               |
| `id`                                    | `id`        | The settings id, a string                                             |
| `languages`                             | `languages` | The active languages, at most 50, each `code`, `name` and `isDefault` |
| `style`                                 | `style`     | The colours, below                                                    |

`style` (`KindlyEventStyle`) has `background`, `buttonBackground`, `buttonText`, `headerBackground`, `headerText`, `inputBackground`, `inputText`, `userMessageBackground`, `userMessageTextColor`, `botMessageBackground` and `botMessageTextColor`, each a hex colour string or null

### message

`newMessage` and `chatLog`, the last 50 messages. React Native's `newMessage` can be `null`. A message (`KindlyEventMessage` on iOS and Android, `KindlyWebViewMessage` in React Native):

| iOS and Android | JSON           | Content                                                                                                           |
| --------------- | -------------- | ----------------------------------------------------------------------------------------------------------------- |
| `id`            | `id`           | The message id. A message the user just typed carries a `tempId-...` id until the server confirms it              |
| `chatId`        | `chatId`       | The chat id, or null                                                                                              |
| `text`          | `text`         | The text, at most 4,000 characters, or null                                                                       |
| `sender`        | `sender`       | `USER`, `BOT`, `AGENT`, `SYSTEM` or `UNKNOWN` (`KindlyEventMessageSender`)                                        |
| `created`       | `created`      | When it was created: a `Date` on iOS and Android, ISO 8601 text in JSON, or null                                  |
| `isFromUser`    | `fromBot`      | The typed models say whether the user sent it, the JSON whether the bot did                                       |
| `exchangeID`    | `exchangeId`   | The exchange id, or null                                                                                          |
| `exchangeType`  | `exchangeType` | `dialogue`, `fallback`, `greeting`, `system_dialogue` or `user_says` (`KindlyEventExchangeType`), or null         |
| `videoSource`   | `videoSource`  | A video URL, without its query, or null                                                                           |
| `buttons`       | `buttons`      | At most 20 buttons, below                                                                                         |
| `images`        | `images`       | At most 20, each `url` (without its query), `altText` and `id`                                                    |
| `attachments`   | `attachments`  | At most 10, each `id`, `name`, `format`, `status`, `sender` and `storagePath`. The download URL is never included |

A button (`KindlyEventButton`, `KindlyWebViewButton`):

| Field             | Content                                            |
| ----------------- | -------------------------------------------------- |
| `id`              | The button id, or null                             |
| `type`            | The button type, for example `quick_reply`         |
| `label`           | The label, at most 500 characters                  |
| `value`           | The value, or null. A URL value has lost its query |
| `index`           | Its position, or null                              |
| `hasBeenSelected` | Whether the user picked it                         |
| `context`         | Flat context, at most 20 primitive values, or null |

### buttonClick

`button`, `buttonType`, `lastMessage` and `chatLog`. A bot button of type `privacy_export` also starts a download of the user's data, see [Uploads and Downloads](https://docs.kindly.ai/sdk/web-view-sdk-uploads-and-downloads)

### ui

| iOS and Android (`KindlyEventUIAction`)                                        | JSON           | React Native `detail`           | Content                                                                                                                |
| ------------------------------------------------------------------------------ | -------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `type`                                                                         | `type`         | `action`, and `rawType` as sent | The action, below                                                                                                      |
| `source`                                                                       | `source`       | `source`                        | `user`, or `programmatic` when a call to the SDK caused it                                                             |
| `chatId`                                                                       | `chatId`       | `chatId`                        | The chat id. When there was none yet it is empty in the typed models and React Native's `detail`, and null in the JSON |
| `value`                                                                        | `value`        | `value`                         | Extra data, the fields below are read from it                                                                          |
| `languageCode`, `languageName`, `previousLanguageCode`, `previousLanguageName` | inside `value` | the same names                  | `change-language`                                                                                                      |
| `feedbackType`, `feedbackValue`                                                | inside `value` | the same names                  | `show-feedback`, `submit-feedback` and `dismiss-feedback`                                                              |
| `nextPage`                                                                     | inside `value` | `nextPage`                      | `open-chatbubble`: `chat` or `welcome-page`                                                                            |

The actions, with the widget value that JSON, React Native and `rawUiAction` use:

| 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 |

Where to read it: iOS `action.type` (`KindlyUIActionType`), Android `action.type` (`KindlyUIActionType`), Flutter `event.uiAction` (`KindlyWebViewUiAction`) with the source in `event.uiSource`, React Native `event.uiAction` with the source in `event.detail.source`. Flutter and React Native report an action they do not know as `unknown`, with the widget value in Flutter's `rawUiAction` and React Native's `detail.rawType`. iOS and Android drop it

`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`

### globalIcon

iOS and Android (`KindlyEventGlobalIcon`): `type`, `source` and `value`, the icon. JSON and React Native: `type` and `icon`, with `id`, `title`, `altText`, `imageUrl`, `type` and `action`, or null

### state

| 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                                           |

| `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`. The same changes also arrive as `state` events

***

## Limits and data handling

- The SDK caps each detail itself: at most 4,000 characters of message text, the last 50 messages of the chat log, 20 buttons, 20 images and 10 attachments per message, labels and other short strings at 500 characters
- 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: never run it as code, build a query from it or open a URL from it without checking it
- Message events carry what the user wrote, so treat them as user data and keep them out of logs and analytics

***

## Worked examples

### React to a bot reply

```swift
KindlyWebViewSDK.events
    .sink { event in
        guard case let .message(newMessage, _) = event.detail, newMessage.sender == .bot else { return }
        // a bot reply arrived while the chat is open
    }
    .store(in: &subscriptions)
```

```kotlin
import ai.kindly.webview.models.KindlyEventDetail
import ai.kindly.webview.models.KindlyEventMessageSender

lifecycleScope.launch {
    KindlyWebViewSDK.events.collect { event ->
        val detail = event.detail
        if (detail is KindlyEventDetail.Message && detail.newMessage.sender == KindlyEventMessageSender.BOT) {
            // a bot reply arrived while the chat is open
        }
    }
}
```

```dart
KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
  final Object? newMessage = event.detail['newMessage'];
  if (event.type == KindlyWebViewEventType.message && newMessage is Map && newMessage['sender'] == 'BOT') {
    // a bot reply arrived while the chat is open
  }
});
```

```typescript
KindlyWebViewSDK.addEventListener((event) => {
  if (event.type === 'message' && event.detail.newMessage?.sender === 'BOT') {
    // a bot reply arrived while the chat is open
  }
});
```

### Follow the chat language

```swift
KindlyWebViewSDK.events
    .sink { event in
        if case let .ui(action) = event.detail, action.type == .changeLanguage, let code = action.languageCode {
            print("chat language", code)
        }
    }
    .store(in: &subscriptions)
```

```kotlin
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.CHANGE_LANGUAGE) {
            Log.d("Chat", "chat language ${detail.action.languageCode}")
        }
    }
}
```

```dart
KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
  final Object? value = event.detail['value'];
  if (event.uiAction == KindlyWebViewUiAction.changeLanguage && value is Map) {
    debugPrint('chat language ${value['languageCode']}');
  }
});
```

```typescript
KindlyWebViewSDK.addEventListener((event) => {
  if (event.type === 'ui' && event.uiAction === 'change-language') {
    console.log('chat language', event.detail.languageCode);
  }
});
```

### Know when the user leaves the chat

`close-chatbubble` arrives when the user minimises the chat, swipes the sheet down on iOS or presses back on Android, and when the app calls `closeChat`, `endChat` or `kill`. To catch every way the chat screen goes away, follow `isChatDisplayed` in the state instead, which turns `false` whenever the screen is gone

***

## Threading notes

- iOS: `events` and `state` emit on the main thread, so UI work in `sink` is safe
- Android: the collecting block runs where the collector runs, so collect in `lifecycleScope` for UI work, and the SDK's calls can be made from any thread
- Flutter: listeners run in the Dart isolate that subscribed, normally the main one
- React Native: listeners run on the JavaScript thread
