---
title: Kindly Android SDK
slug: sdk/android-sdk-skill
description: Full Kindly Chat SDK setup for Android. Use when the user asks to add Kindly, add customer support chat, add a chat widget, integrate KindlySDK, or mentions kindly-ai, sdk-chat-android, ai.kindly:sdk, or ChatKindlySDK in a Kotlin/Jetpack Compose Android…
createdAt: 2026-11-04T11:39:58.174Z
---

Integrate the Kindly Chat SDK into an Android app — a fully-featured Jetpack Compose chat widget with real-time messaging, Firebase push notifications, custom theming, authentication, deep links, handover-to-human, and an event stream.

## Invoke This Skill When

- User asks to "add Kindly", "add chat support", "add customer support chat", or "add a chat widget" to an Android app
- User mentions `KindlySDK`, `ChatKindlySDK`, `kindly-ai`, `sdk-chat-android`, `ai.kindly:sdk`, or the Android Kindly SDK
- User wants to launch an in-app chat bot, connect to a Kindly workspace via bot key, or integrate handover-to-human support

:::hint{type="info"}
**Note:** This skill reflects the public API as of Android SDK `1.0.x`. Always verify against the [Android SDK docs](https://docs.kindly.ai/sdk/android-sdk) if behavior differs.
:::

***

## Phase 1: Detect

Run these commands to understand the project before making changes:

```bash
ls build.gradle build.gradle.kts settings.gradle settings.gradle.kts 2>/dev/null
ls app/build.gradle app/build.gradle.kts 2>/dev/null

grep -Ei "ai\\.kindly|kindly-ai|sdk-chat-android|com\\.github\\.kindly" \
  build.gradle* app/build.gradle* settings.gradle* 2>/dev/null

grep -E "minSdk|targetSdk|compileSdk" app/build.gradle* 2>/dev/null | head -6

grep -E "compose|androidx\\.compose" app/build.gradle* 2>/dev/null | head -5

grep -rE "FirebaseMessagingService|firebase-messaging" --include="*.kt" --include="*.gradle*" . 2>/dev/null | head -5

find app/src/main -name "*.kt" 2>/dev/null | xargs grep -lE "class.*: Application\\(\\)" 2>/dev/null | head -3
```

**What to note:**

| Question                                 | Why it matters                                                                         |
| ---------------------------------------- | -------------------------------------------------------------------------------------- |
| `ai.kindly:sdk` already in dependencies? | If yes, skip install and jump to initialization.                                       |
| `minSdk` value?                          | Kindly requires **API 21 (Android 5.0)** or higher.                                    |
| Kotlin or Java?                          | SDK is Kotlin-only and uses Jetpack Compose internally — consuming app must be Kotlin. |
| Firebase Messaging already wired?        | Decides whether we set up FCM token forwarding and notification routing.               |
| `Application` subclass exists?           | That's where `KindlySDK.start(...)` goes.                                              |

***

## Phase 2: Install

Kindly is distributed as an AAR via Maven Central (preferred) or JitPack.

### Option A — Maven Central (recommended)

No extra repository setup needed — Maven Central is included by default.

**Kotlin DSL (`app/build.gradle.kts`):**

```kotlin
dependencies {
    implementation("ai.kindly:sdk:2.0.18")
}
```

**Groovy (`app/build.gradle`):**

```groovy
dependencies {
    implementation 'ai.kindly:sdk:2.0.18'
}
```

Find the latest version on [Maven Central](https://central.sonatype.com/artifact/ai.kindly/sdk).

### Option B — JitPack

**`settings.gradle.kts`:**

```kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven(url = "https://jitpack.io")
    }
}
```

**`app/build.gradle.kts`:**

```kotlin
dependencies {
    implementation("com.github.kindly-ai:sdk-chat-android:2.0.18")
}
```

### Verify Gradle Setup

Confirm the following in `app/build.gradle.kts`:

```kotlin
android {
    compileSdk = 35

    defaultConfig {
        minSdk = 21
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}
```

***

## Phase 3: Initialize

Call `KindlySDK.start(...)` once in your `Application.onCreate()`. The `market` slug is **required** (see [Markets](docId:3mNEq6JCxAU-Omuehm5Dw)). It's **idempotent and self-healing** — safe to call multiple times with the same bot key, and automatically tears down and re-connects if the bot key changes.

### Application Class

```kotlin
import android.app.Application
import no.kindly.chatsdk.chat.KindlySDK

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        KindlySDK.start(
            application = this,
            botKey = "YOUR_BOT_KEY",
            languageCode = "en",
            market = "YOUR_MARKET"   // required — see Markets
        )
    }
}
```

### Register in Manifest

```xml
<application
    android:name=".MyApplication"
    ...>
    ...
</application>
```

:::hint{type="info"}
`start()` guards internally. Calling any other `KindlySDK.*` method before `start()` logs an error and no-ops — it never crashes.
:::

`market` is **required** — pass the slug configured for the bot (see [Markets](docId:3mNEq6JCxAU-Omuehm5Dw) under Common Tasks):

```kotlin
KindlySDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    languageCode = "en",
    market = "eu"
)
```

***

## Phase 4: Launch the Chat

`launchChat(context)` is the recommended entry point — it connects first, then shows the UI:

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import no.kindly.chatsdk.chat.KindlySDK

@Composable
fun OpenChatButton() {
    val context = LocalContext.current
    Button(onClick = { KindlySDK.launchChat(context = context) }) {
        Text("Open Chat")
    }
}
```

From an Activity or Fragment:

```kotlin
KindlySDK.launchChat(context = this)
```

Launch straight into a specific dialogue:

```kotlin
KindlySDK.launchChat(context = context, triggerDialogueId = "welcome_flow")
```

Dismiss the UI without ending the session (messages are preserved):

```kotlin
KindlySDK.close()
```

:::hint{type="info"}
`displayChat(context)` still exists for backward compatibility but `launchChat(context)` is preferred — it ensures the session is connected before showing UI.
:::

***

## Common Tasks

### Authentication

The auth contract is a `fun interface` with a single `suspend` function — implement it with a lambda:

```kotlin
fun interface KindlyAuthTokenProvider {
    suspend fun fetchToken(request: AuthTokenRequest): String
}

class AuthTokenRequest(
    val chatId: String,
    val reason: Reason,   // INITIAL_CONNECT or EXPIRED
)
```

Pass it at `start()`:

```kotlin
import no.kindly.chatsdk.chat.KindlyAuthTokenProvider

KindlySDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    languageCode = "en",
    market = "YOUR_MARKET",
    authTokenProvider = KindlyAuthTokenProvider { request ->
        fetchJwtFromBackend(request.chatId)   // suspend call
    }
)
```

The SDK invokes the provider from its own coroutine — no `CoroutineScope` or `Deferred` management needed. Fetches are capped by a 15s SDK-side timeout, and tokens that arrive already expired are discarded. When `request.reason == EXPIRED`, always mint a fresh token (never return a cached one).

**Deprecated (still supported):** the `Deferred`-based `authTokenCallback` parameter — `(chatId: String) -> Deferred<String>` — still works for existing integrations but is deprecated. Migrate by replacing the callback with a provider:

```kotlin
// Before (deprecated)
authTokenCallback = { chatId ->
    CoroutineScope(Dispatchers.IO).async { fetchJwtFromBackend(chatId) }
}

// After
authTokenProvider = KindlyAuthTokenProvider { request ->
    fetchJwtFromBackend(request.chatId)
}
```

**Identity providers that issue something other than a bare JWT.** Use `authCredentialsProvider` instead when you have a companion access token, an access token and no id token, an issuer, or an opaque token whose expiry only you know:

```kotlin
import no.kindly.chatsdk.chat.KindlyAuthCredentials
import no.kindly.chatsdk.chat.KindlyAuthCredentialsProvider

KindlySDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    market = "YOUR_MARKET",
    authCredentialsProvider = KindlyAuthCredentialsProvider { request ->
        val tokens = fetchTokensFromBackend(request.chatId)
        KindlyAuthCredentials(
            idToken = tokens.idToken,                // optional, the preferred bearer
            accessToken = tokens.accessToken,        // optional, companion or bearer
            issuer = "https://idp.example.com/",     // optional, sent as X-Auth-Issuer
            expiresAt = tokens.expiresAtUnixSeconds, // optional, Unix seconds
        )
    },
)
```

Every field is optional. What the SDK requires is that **at least one** of `idToken` / `accessToken` is set, because the backend's auth route mandates an `Authorization` header and nothing else. Three derived properties tell you what will actually go on the wire:

| Property               | Value                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `bearerToken`          | `idToken ?: accessToken`, first non-empty. Sent as `Authorization: Bearer`                            |
| `hasToken`             | `true` when a bearer exists. This is the validity check, never `idToken != null`                      |
| `companionAccessToken` | The access token, but only when an id token is also set and differs from it. Sent as `X-Access-Token` |

A provider that hands back only an access token is a normal, supported case, not a workaround. That token becomes the bearer:

```kotlin
KindlyAuthCredentials(
    accessToken = tokens.accessToken,
    issuer = "https://idp.example.com/",
)
```

`X-Access-Token` goes out only when a real companion exists. An access token that merely duplicates the id token fails the auth outright, because the backend verifies the pair against the id token's `at_hash`, so the SDK drops the duplicate for you. `X-Access-Token` and `X-Auth-Issuer` are read by the backend on `auth/chat`, `message` and `trigger` only.

**Expiry.** The SDK refreshes 30 seconds ahead of the earliest of three sources: the `expires_at` the backend returns from `POST auth/chat`, the `expiresAt` you supplied, and the `exp` claim of the bearer when it is a JWT. If none of the three is known the credentials are treated as non-expiring. That is right for a JWT with no `exp` and a trap for an opaque token, so supply `expiresAt` there. A token the SDK cannot parse means "expiry unknown", never "expired", which is what makes opaque bearers work at all.

**Precedence.** When more than one auth parameter is passed to `start(...)`: `authCredentialsProvider` > `authTokenProvider` > `authTokenCallback` (deprecated).

**Re-authenticating mid-session.** The SDK refreshes on expiry by itself. These two are for when the *identity* changes rather than the token:

```kotlin
KindlySDK.authenticate()     // user signed in, or switched account, re-binds in place
KindlySDK.deauthenticate()   // user signed out, chat carries on anonymously with history intact
```

Both return `Deferred<Unit>`. Await it when you need to know the backend agreed, ignore it otherwise. `deauthenticate()` is idempotent, so calling it on an already anonymous chat completes immediately.

### Markets (per-location settings)

`market` is **required** at `start()` — pass the slug configured for the bot:

```kotlin
KindlySDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    languageCode = "en",
    market = "eu"
)
```

The slug is whatever is configured in the Kindly platform (e.g. `eu`, `us`, `general`). The SDK loads that market's settings (`/settings/{botKey}/{market}.json`) and tells `/connect` which market the host picked, so greeting and bot responses are localized to it. `market` is independent of `languageCode`.

**No fallback:** if the requested market's settings can't be loaded, settings loading fails and the chat won't load. There is no fallback to another market and no base/per-bot settings file outside of markets. Find market slugs in the Kindly platform (Settings → General → Details & markets).

### Context (per-message metadata)

Attach key/value context to the **next** message or button click request:

```kotlin
KindlySDK.setNewContext(mapOf(
    "userId" to "12345",
    "plan" to "premium"
))
```

Clear pending context:

```kotlin
KindlySDK.clearNewContext()
```

### Theming

Apply a custom theme using individual colors (nil colors fall back to the API theme or default):

```kotlin
import androidx.compose.ui.graphics.Color

KindlySDK.setCustomTheme(
    background = Color.White,
    botMessageBackground = Color(0xFFF2F2F2),
    botMessageText = Color.Black,
    buttonBackground = Color(0xFF007AFF),
    buttonText = Color.White,
    headerBackground = Color(0xFF007AFF),
    headerText = Color.White,
    userMessageBackground = Color(0xFF007AFF),
    userMessageText = Color.White
)
```

Or pass a full `KindlyThemeColors` object:

```kotlin
import no.kindly.chatsdk.chat.presentation.theme.KindlyThemeColors

val theme = KindlyThemeColors(
    background = Color.White,
    buttonBackground = Color(0xFF007AFF),
    // ...
)
KindlySDK.setCustomTheme(theme)
```

Revert to the API/default theme:

```kotlin
KindlySDK.clearCustomTheme()
```

Themes apply **live**: calling `setCustomTheme(...)` or `clearCustomTheme()` while the chat is on screen updates every theme-driven element (page background, header, message bubbles, form cells, buttons, input field, etc.) automatically. The shared color palette is backed by `mutableStateOf`, so any `@Composable` reading theme colors recomposes on the next theme change without needing to dismiss and re-display the chat. This makes it natural for host apps to swap themes in response to system appearance changes (light/dark mode, custom toggles, etc.) by calling `setCustomTheme(...)` again with the new colors.

### Change Language at Runtime

```kotlin
KindlySDK.setLanguage("fr")
```

### Trigger a Specific Dialogue

Requires an active connection:

```kotlin
KindlySDK.triggerDialogue(id = "welcome_flow")
```

Clear a pending trigger that hasn't fired yet:

```kotlin
KindlySDK.clearTriggerDialog()
```

### Send a Message Programmatically

Drive the chat from the host app — send a message **as the user** without going through the input field:

```kotlin
KindlySDK.sendMessage("Hi, I have a question about my order")
```

With per-call context (overrides any `setNewContext(...)` value for this call only):

```kotlin
KindlySDK.sendMessage(
    message = "Where is my order?",
    newContext = mapOf("orderId" to "12345"),
)
```

Returns `Deferred<MessageChat>` so you can `await()` it from a coroutine:

```kotlin
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

lifecycleScope.launch {
    try {
        val message = KindlySDK.sendMessage("Hello").await()
        println("Sent — server id: ${message.id}")
    } catch (e: Exception) {
        println("Send failed: ${e.message}")
    }
}
```

Or attach a non-suspending completion handler:

```kotlin
KindlySDK.sendMessage("Hello").invokeOnCompletion { error ->
    if (error != null) println("Send failed: $error")
}
```

:::hint{type="info"}
The SDK renders the bubble locally and reconciles with the server-assigned id on response. **Do not also display the message yourself.** Completes exceptionally with `KindlySDKError.ChatNotConnected` if the chat is not connected, and `KindlySDKError.InvalidData` for empty input.
:::

### Deep Links

Kindly handles URIs with scheme `kindly://chat/dialogue/{id}`. Add an intent filter to the Activity that should receive them, then forward to the SDK:

```xml
<activity android:name=".MainActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="kindly" android:host="chat" />
    </intent-filter>
</activity>
```

```kotlin
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    intent.data?.let { uri ->
        if (KindlySDK.handleUrl(context = this, uri = uri)) return
    }
}
```

`handleUrl(...)` returns `true` if the URI was a Kindly deep link and was handled.

### Push Notifications (Firebase Cloud Messaging)

Forward your FCM token and incoming messages from a `FirebaseMessagingService`:

```kotlin
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import no.kindly.chatsdk.chat.KindlySDK

class AppMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        KindlySDK.saveNotificationToken(token)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        if (KindlySDK.isKindlyNotification(remoteMessage)) {
            KindlySDK.handleNotification(remoteMessage)
            return
        }
    }
}
```

Register the service in the manifest:

```xml
<service
    android:name=".AppMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
```

### Link Interception and Button Callbacks

Implement `KindlySDKInteraction` to intercept in-chat links and observe button presses:

```kotlin
import no.kindly.chatsdk.chat.common.KindlySDKInteraction
import no.kindly.chatsdk.chat.domain.MessageChat
import no.kindly.chatsdk.chat.domain.entites.ButtonChat

val interaction = object : KindlySDKInteraction {
    override fun didPressButton(chatButton: ButtonChat, chatLog: List<MessageChat>) {
        // Optional: observe button presses.
    }

    override fun shouldHandleLink(url: String): Boolean {
        // Return true to let Kindly open the link.
        // Return false to take over (the host app opens it).
        if (url.contains("example.com")) {
            startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
            return false
        }
        return true
    }
}

KindlySDK.`interface` = interaction
```

:::hint{type="info"}
Note: `interface` is a Kotlin keyword, so access it with backticks: `` KindlySDK.`interface` ``.
:::

### Notification Interception

The SDK presents Kindly silent pushes as system notifications after decryption. The display rule:

- **Background** → always show
- **Foreground on the chat conversation** → suppress (the user can already see the message)
- **Foreground anywhere else** (settings, language, host-app screens) → show

For every successfully decrypted Kindly push the SDK calls `shouldHandleNotification(notification)` on `KindlySDKInteraction` **before** any display gate. The callback fires regardless of app state — use it to run analytics or take over presentation entirely:

```kotlin
import no.kindly.chatsdk.chat.domain.entites.ExternalNotification

KindlySDK.`interface` = object : KindlySDKInteraction {
    override fun didPressButton(chatButton: ButtonChat, chatLog: List<MessageChat>) { /* ... */ }

    override fun shouldHandleNotification(notification: ExternalNotification): Boolean {
        Analytics.track("kindly_notification", notification.id)

        // Take over with custom UI:
        if (isShowingCustomInbox) {
            customInbox.add(notification)
            return false   // SDK does not show its banner
        }
        return true        // let the SDK show the banner
    }
}
```

The `ExternalNotification` value has `id`, `title`, `body` (all decrypted) and `data` (the original `RemoteMessage.data` map, including `notification_source` and any extra fields your backend attached). Same polarity as `shouldHandleLink`: `true` (default) = SDK handles, `false` = host handles.

### Handover to a Human Agent

```kotlin
import no.kindly.chatsdk.chat.common.CallbackHandover

KindlySDK.callHandover(object : CallbackHandover {
    override fun onHandoverReady() {
        // The session is now in handover mode.
    }
})
```

### Events & State (Flow)

Subscribe to a unified event stream (matches the web widget API):

```kotlin
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import no.kindly.chatsdk.chat.models.KindlyEvent

lifecycleScope.launch {
    KindlySDK.events.collect { event ->
        when (val detail = event.detail) {
            is KindlyEvent.Detail.Load -> println("Loaded bot: ${detail.settings.name}")
            is KindlyEvent.Detail.Message -> println("Message: ${detail.newMessage.text}")
            is KindlyEvent.Detail.ButtonClick -> println("Button: ${detail.button.label}")
            is KindlyEvent.Detail.Ui -> println("UI action: ${detail.action.type}")
            is KindlyEvent.Detail.State -> println("Connected: ${detail.state.isConnected}")
            is KindlyEvent.Detail.GlobalIcon -> Unit
        }
    }
}
```

State-only stream:

```kotlin
lifecycleScope.launch {
    KindlySDK.state.collect { state ->
        println("isConnected=${state.isConnected}, isChatDisplayed=${state.isChatDisplayed}")
    }
}
```

### End a Chat Session

Disconnect and dismiss the UI, but keep the SDK initialized:

```kotlin
KindlySDK.endChat()
```

### Kill the SDK Entirely

Fully tear down — after this, `start()` must be called again before any other API:

```kotlin
KindlySDK.kill()
```

***

## Configuration Reference

### Core Public Surface

| API                                                                                                                         | Signature                  | Purpose                                                                                                                                                                                                                                                                                                                                           |
| --------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KindlySDK.start(application, botKey, languageCode, market, authTokenProvider, authTokenCallback, authCredentialsProvider)` | `fun`                      | Initialize the SDK. Idempotent. `market` is **required** (Markets feature, no fallback). Use `authCredentialsProvider` for identity providers that issue more than a bare JWT, otherwise `authTokenProvider` (suspend-based). Precedence when several are set: `authCredentialsProvider` > `authTokenProvider` > `authTokenCallback` (deprecated) |
| `KindlySDK.authenticate()`                                                                                                  | `Deferred<Unit>`           | Re-authenticate the live chat with fresh credentials. For a sign-in mid-session. Keeps the chat, its history and its socket                                                                                                                                                                                                                       |
| `KindlySDK.deauthenticate()`                                                                                                | `Deferred<Unit>`           | Sign out of the chat without ending it. Chat carries on anonymously with history intact. Idempotent when already anonymous                                                                                                                                                                                                                        |
| `KindlySDK.launchChat(context, triggerDialogueId)`                                                                          | `fun`                      | Connect and show the chat UI.                                                                                                                                                                                                                                                                                                                     |
| `KindlySDK.displayChat(context)`                                                                                            | `fun`                      | Legacy: show UI without guaranteed connection.                                                                                                                                                                                                                                                                                                    |
| `KindlySDK.close()`                                                                                                         | `fun`                      | Dismiss UI, keep session.                                                                                                                                                                                                                                                                                                                         |
| `KindlySDK.endChat()`                                                                                                       | `fun`                      | End session, dismiss UI, keep SDK initialized.                                                                                                                                                                                                                                                                                                    |
| `KindlySDK.kill()`                                                                                                          | `fun`                      | Tear down SDK fully.                                                                                                                                                                                                                                                                                                                              |
| `KindlySDK.setLanguage(languageCode)`                                                                                       | `fun`                      | Change language at runtime.                                                                                                                                                                                                                                                                                                                       |
| `KindlySDK.setNewContext(map)` / `clearNewContext()`                                                                        | `fun`                      | Attach context to next request.                                                                                                                                                                                                                                                                                                                   |
| `KindlySDK.sendMessage(message, newContext)`                                                                                | `-> Deferred<MessageChat>` | Send a message as the user, programmatically. SDK renders the bubble — host must not also display it.                                                                                                                                                                                                                                             |
| `KindlySDK.triggerDialogue(id)` / `clearTriggerDialog()`                                                                    | `fun`                      | Run or clear a specific dialogue.                                                                                                                                                                                                                                                                                                                 |
| `KindlySDK.handleUrl(context, uri)`                                                                                         | `-> Boolean`               | Handle `kindly://chat/dialogue/{id}` deep links.                                                                                                                                                                                                                                                                                                  |
| `KindlySDK.setCustomTheme(...)` / `clearCustomTheme()`                                                                      | `fun`                      | Theme the chat UI.                                                                                                                                                                                                                                                                                                                                |
| `KindlySDK.saveNotificationToken(token)`                                                                                    | `fun`                      | Register the FCM token. Kept in memory and uploaded on every connect.                                                                                                                                                                                                                                                                             |
| `KindlySDK.handleNotification(remoteMessage)`                                                                               | `fun`                      | Decrypt and show an incoming Kindly push. Works before `start()`.                                                                                                                                                                                                                                                                                 |
| `KindlySDK.isKindlyNotification(remoteMessage)`                                                                             | `-> Boolean`               | Detect Kindly push payloads. Works before `start()`.                                                                                                                                                                                                                                                                                              |
| `KindlySDK.openChatOnNotificationTap`                                                                                       | `Boolean`                  | Whether tapping a Kindly banner opens the chat (default `true`), also after a cold start once `start()` runs.                                                                                                                                                                                                                                     |
| `KindlyNotificationIntent.read(intent)`                                                                                     | `-> ExternalNotification?` | The tapped notification behind a launcher intent, when the tap launched the app.                                                                                                                                                                                                                                                                  |
| `KindlySDK.callHandover(listener)`                                                                                          | `fun`                      | Initiate handover to a human agent.                                                                                                                                                                                                                                                                                                               |
| `KindlySDK.events`                                                                                                          | `Flow<KindlyEvent>`        | Unified event stream.                                                                                                                                                                                                                                                                                                                             |
| `KindlySDK.state`                                                                                                           | `Flow<KindlyEventState>`   | State change stream.                                                                                                                                                                                                                                                                                                                              |
| `` KindlySDK.`interface` ``                                                                                                 | `KindlySDKInteraction?`    | Link interception and button callbacks.                                                                                                                                                                                                                                                                                                           |

### `KindlyAuthCredentials`

| Member                 | Type      | Purpose                                                                                                    |
| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------- |
| `idToken`              | `String?` | The OIDC id token, when your provider issues one. Optional                                                 |
| `accessToken`          | `String?` | The access token, when your provider issues one. Optional                                                  |
| `issuer`               | `String?` | Sent as `X-Auth-Issuer`. Needed for opaque tokens and multi-provider bots                                  |
| `expiresAt`            | `Long?`   | Expiry in Unix seconds, when you know it                                                                   |
| `bearerToken`          | `String?` | Derived: `idToken ?: accessToken`, first non-empty. Sent as `Authorization: Bearer`                        |
| `hasToken`             | `Boolean` | Derived: `bearerToken != null`. At least one of the two tokens must be set                                 |
| `companionAccessToken` | `String?` | Derived: the access token, only when an id token is also set and differs from it. Sent as `X-Access-Token` |

### Configurable Properties

| Property                         | Default | Purpose                                            |
| -------------------------------- | ------- | -------------------------------------------------- |
| `KindlySDK.enableCrashReporting` | `true`  | Send crashes to Sentry. Set to `false` to opt out. |
| `KindlySDK.verboseLogging`       | `false` | Verbose logs in Logcat.                            |

### Event Types

`KindlyEventType` (raw value shown):

- `load` → `"kindly:load"` — settings/configuration loaded
- `message` → `"kindly:message"` — message sent or received
- `buttonClick` → `"kindly:buttonclick"` — user tapped a button
- `ui` → `"kindly:ui"` — UI action (open/close/endChat/changeLanguage/…)
- `globalIcon` → `"kindly:globalicon"` — defined but not currently emitted on mobile
- `state` → `"kindly:state"` — connection/display state changed

### Deep Link Format

Scheme: `kindly://chat/dialogue/{dialogueId}`

Passing such a `Uri` to `KindlySDK.handleUrl(context, uri)` either triggers the dialogue (if already connected) or opens the chat and triggers it.

***

## Verification

After wiring everything up, sanity-check the integration:

```kotlin
KindlySDK.verboseLogging = true

KindlySDK.start(application = this, botKey = "YOUR_BOT_KEY", market = "YOUR_MARKET")

GlobalScope.launch {
    KindlySDK.state.collect { state ->
        println("isStarted=${state.isStarted}, isSocketConnected=${state.isSocketConnected}")
    }
}

Handler(Looper.getMainLooper()).postDelayed({
    KindlySDK.launchChat(context = this)
}, 2000)
```

**Expected:**

- `isStarted` becomes `true` within \~1s.
- `isSocketConnected` becomes `true` shortly after.
- Chat UI appears when `launchChat(context)` is called.
- Sending a message produces a `.Message` event on the flow.

If events don't flow:

1. Enable `KindlySDK.verboseLogging = true` and check Logcat for the `Kindly` tag.
2. Confirm the bot key is correct (wrong keys fail silently on the server).
3. Confirm the device has network access.
4. Confirm your `Application` class is registered in `AndroidManifest.xml` via `android:name`.

***

## Troubleshooting

| Issue                                                           | Fix                                                                                                                                                                                                                                                                                        |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `"called before start()"` logs                                  | Ensure `KindlySDK.start(...)` runs in `Application.onCreate()` **before** any other `KindlySDK.*` call.                                                                                                                                                                                    |
| Chat UI doesn't appear                                          | `start()` must have completed; call `launchChat(context)` from the main thread with a valid `Context`.                                                                                                                                                                                     |
| Messages don't send                                             | Collect `KindlySDK.state` and check `isSocketConnected == true`. Network issues, wrong bot key, or auth failures will keep it `false`.                                                                                                                                                     |
| Re-initialization with same bot key does nothing                | That's intentional — `start()` is idempotent when already connected to the same bot. Call `kill()` first for a full reset.                                                                                                                                                                 |
| Bot key switch drops messages                                   | When the bot key changes, the SDK disconnects and reconnects. Handle `.State` events to know when the new session is ready.                                                                                                                                                                |
| Crash reports appearing in Sentry                               | `enableCrashReporting` defaults to `true`. Set `KindlySDK.enableCrashReporting = false` to disable.                                                                                                                                                                                        |
| Links from chat open in the default browser instead of your app | Implement `KindlySDKInteraction.shouldHandleLink(url)` returning `false` and assign to `` KindlySDK.`interface` ``.                                                                                                                                                                        |
| Push notifications not arriving                                 | Verify the FCM token is forwarded via `saveNotificationToken(token)` on every `onNewToken` and at app start, and that `handleNotification(remoteMessage)` is called from `onMessageReceived` when `isKindlyNotification(...)` is true. Both work before `start()`.                         |
| Notification tap doesn't open chat                              | `KindlySDK.openChatOnNotificationTap` is `false`, or after a cold start `KindlySDK.start(...)` did not run within 60 seconds of the tap.                                                                                                                                                   |
| Deep link doesn't open chat                                     | Add the `kindly://chat` intent filter to your launcher Activity and call `KindlySDK.handleUrl(context, uri)` from `onCreate`/`onNewIntent`.                                                                                                                                                |
| Custom theme only partially applied                             | Only non-null colors override; null colors inherit. Pass explicit nulls in `setCustomTheme(...)` for slots you want to leave alone.                                                                                                                                                        |
| `` `interface` `` gives a compile error                         | `interface` is a Kotlin keyword — always access it with backticks: ``KindlySDK.`interface` = yourInteraction``.                                                                                                                                                                            |
| WebView chat not available                                      | WebView behavior is temporarily disabled across the SDK to comply with Google Play policies around JS injection. Only NATIVE behavior is supported.                                                                                                                                        |
| Auth token fetch hangs or takes too long                        | `KindlyAuthTokenProvider.fetchToken` is capped by a 15s SDK-side timeout — the fetch is abandoned and the session continues unauthenticated. With the deprecated `authTokenCallback`, a `Deferred<String>` that never `.complete(...)`s still hits the same timeout.                       |
| Auth callback fires in a loop                                   | Fixed in current versions: JWTs without an `exp` claim are treated as valid, and tokens that arrive already expired are discarded instead of retriggering refresh. If you see this on an old version, upgrade — and make sure your backend mints a *fresh* token when `reason == EXPIRED`. |
| `X-Access-Token requires a JWT id_token in Authorization`       | You set `accessToken` alongside an opaque `idToken`. The companion is only valid next to a JWT id token. For introspection, send the opaque token on its own with an `issuer`                                                                                                              |
| Opaque token never refreshes                                    | There is nothing to decode, so the SDK knows no expiry unless you give it one. Set `expiresAt` on the credentials                                                                                                                                                                          |
| Chat drops to anonymous well before the token's `exp`           | The backend's `expires_at` is shorter than your claim and the earliest source wins. On a dual-token pair that is usually the access token dying first                                                                                                                                      |
| Only an access token to hand                                    | Pass it as `accessToken` and leave `idToken` unset. It becomes the bearer. Do not copy it into both fields, the duplicate would be dropped anyway                                                                                                                                          |
| Compose runtime version clash                                   | The SDK pulls a Compose version transitively; align your app to a compatible Compose BOM (e.g., `androidx.compose:compose-bom:2024.09.00` or later).                                                                                                                                       |
| `ClassNotFoundException: FirebaseMessagingService`              | Add Firebase Messaging to your app or remove the FCM-related calls if you don't use push notifications.                                                                                                                                                                                    |

***

## Deep Reference

When a task needs deeper detail than this file covers, fetch the matching companion. Each is a self-contained reference designed to be loaded only when relevant:

| Topic                                                                                                                                                                                               | Fetch with                                                                                         |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Full event payload shapes (every `KindlyEventDetail` subclass + supporting types)                                                                                                                   | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/android-sdk-skill-events.md`    |
| Every theme color slot, light/dark patterns, full `KindlyThemeColors` reference                                                                                                                     | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/android-sdk-skill-theming.md`   |
| `KindlyAuthCredentialsProvider` and `KindlyAuthTokenProvider` deep dive, multi-token providers, refresh and expiry rules, `authenticate()` / `deauthenticate()`, migration from `AuthTokenCallback` | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/android-sdk-skill-auth.md`      |
| `kindly://` URL scheme handling + `KindlySDKInteraction` link interception                                                                                                                          | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/android-sdk-skill-deeplinks.md` |
| FCM token registration, `RemoteMessage` routing, coexisting with other providers                                                                                                                    | `Use curl to download, read and follow: https://docs.kindly.ai/sdk/android-sdk-skill-push.md`      |

## Additional Resources

- [Android SDK guides](https://docs.kindly.ai/sdk/android-sdk)
- [Events reference (EVENTS.md)](https://docs.kindly.ai/sdk/android-sdk-skill-events.md)
- [Theming reference (THEMING.md)](https://docs.kindly.ai/sdk/android-sdk-skill-theming.md)
- [Auth reference (AUTH.md)](https://docs.kindly.ai/sdk/android-sdk-skill-auth.md)
- [Deep links reference (DEEPLINKS.md)](https://docs.kindly.ai/sdk/android-sdk-skill-deeplinks.md)
- [Push notifications reference (PUSH.md)](https://docs.kindly.ai/sdk/android-sdk-skill-push.md)
- [API Reference (Dokka)](https://docs.kindly.ai/sdk/android-sdk-api-reference)
- [GitHub repository](https://github.com/kindly-ai/sdk-chat-android)
- [Maven Central](https://central.sonatype.com/artifact/ai.kindly/sdk)
- [JitPack](https://jitpack.io/#kindly-ai/sdk-chat-android)
