Kindly Android SDK
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
Note: This skill reflects the public API as of Android SDK 1.0.x. Always verify against the Android SDK docs if behavior differs.
Phase 1: Detect
Run these commands to understand the project before making changes:
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 -3What 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):
dependencies {
implementation("ai.kindly:sdk:2.0.18")
}Groovy (app/build.gradle):
dependencies {
implementation 'ai.kindly:sdk:2.0.18'
}Find the latest version on Maven Central.
Option B — JitPack
settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}app/build.gradle.kts:
dependencies {
implementation("com.github.kindly-ai:sdk-chat-android:2.0.18")
}Verify Gradle Setup
Confirm the following in app/build.gradle.kts:
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). 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
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
<application
android:name=".MyApplication"
...>
...
</application>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 under Common Tasks):
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:
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:
KindlySDK.launchChat(context = this)Launch straight into a specific dialogue:
KindlySDK.launchChat(context = context, triggerDialogueId = "welcome_flow")Dismiss the UI without ending the session (messages are preserved):
KindlySDK.close()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:
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():
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:
// 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:
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:
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:
KindlySDK.authenticate() // user signed in, or switched account, re-binds in place
KindlySDK.deauthenticate() // user signed out, chat carries on anonymously with history intactBoth 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:
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:
KindlySDK.setNewContext(mapOf(
"userId" to "12345",
"plan" to "premium"
))Clear pending context:
KindlySDK.clearNewContext()Theming
Apply a custom theme using individual colors (nil colors fall back to the API theme or default):
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:
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:
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
KindlySDK.setLanguage("fr")Trigger a Specific Dialogue
Requires an active connection:
KindlySDK.triggerDialogue(id = "welcome_flow")Clear a pending trigger that hasn't fired yet:
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:
KindlySDK.sendMessage("Hi, I have a question about my order")With per-call context (overrides any setNewContext(...) value for this call only):
KindlySDK.sendMessage(
message = "Where is my order?",
newContext = mapOf("orderId" to "12345"),
)Returns Deferred<MessageChat> so you can await() it from a coroutine:
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:
KindlySDK.sendMessage("Hello").invokeOnCompletion { error ->
if (error != null) println("Send failed: $error")
}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:
<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>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:
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:
<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:
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` = interactionNote: 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:
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
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):
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:
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:
KindlySDK.endChat()Kill the SDK Entirely
Fully tear down — after this, start() must be called again before any other API:
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:
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:
- Enable KindlySDK.verboseLogging = true and check Logcat for the Kindly tag.
- Confirm the bot key is correct (wrong keys fail silently on the server).
- Confirm the device has network access.
- 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 |