---
title: Flutter SDK
slug: flutter-sdk
createdAt: 2026-11-03T10:14:12.369Z
---

[![pub package](https://img.shields.io/pub/v/kindly.svg)](https://pub.dev/packages/kindly)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Kindly Chat SDK for Flutter — customer support chat widget for iOS and Android applications. Thin Flutter facade over the public Kindly iOS and Android SDKs.

The published package ships the native **iOS xcframework** and **Android AAR** inline, so consumer apps don't need to wire up Swift Package Manager or JitPack themselves.

## Features

- Customisable themes and colours
- Authentication via JWT (callback-based)
- Multi-language support
- Real-time chat
- Push notifications (APNS + FCM)
- Trigger specific dialogues / deep-link URLs
- Programmatic message send
- Context attached to next message or button click
- Native iOS and Android implementations bundled — no extra setup

## Requirements

- Flutter >= 3.0 (Dart >= 2.17)
- iOS >= 17.2 (matches the bundled xcframework — older targets fail at launch with missing dyld symbols)
- Android `minSdk` >= 21, plugin builds with `compileSdk 35` (matches android-source)

## Installation

Add `kindly` to your `pubspec.yaml`:

```yaml
dependencies:
  kindly: ^3.0.26
```

Then `flutter pub get`.

:::BlockQuote
**Migrating from `kindly_sdk`?** This package is the renamed successor to `kindly_sdk`. The public API is identical — only the package name changed. To migrate: bump the dep from `kindly_sdk` to `kindly: ^3.0.26`, and replace `import 'package:kindly_sdk/kindly_sdk.dart'` with `import 'package:kindly/kindly.dart'`. Nothing else needs to change.
:::

### iOS Setup

No additional setup required. The package vendors a signed `KindlySDK.xcframework` and CocoaPods links it automatically when Flutter runs `pod install`. (The framework is named `KindlySDK` rather than `Kindly` so it doesn't case-collide with the `kindly` pod on macOS's case-insensitive filesystem.) The podspec pulls in `Sentry` as its one external dependency, matching what the bundled binary imports.

### Android Setup

No additional setup required. The package ships a `kindlysdk.aar` that Gradle picks up directly via `implementation files("libs/kindlysdk.aar")`.

:::BlockQuote
**Note on manifest merger.** The bundled AAR declares `android:label="@string/app_name"` on its `<application>` element. If your app's `AndroidManifest.xml` also sets `android:label`, Android's manifest merger fails with "Attribute application\@label is also present at \[kindlysdk.aar]". To resolve, add `tools:replace="android:label"` to your `<application>` tag (and declare `xmlns:tools="http://schemas.android.com/tools"` on the root `<manifest>` if not already):

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:label="My App"
        …
        tools:replace="android:label">
        …
    </application>
</manifest>
```
:::

## Usage

### Basic Setup

```dart
import 'package:kindly/kindly.dart';

await KindlySDK.start(
  botKey: 'YOUR_BOT_KEY',
  market: 'general', // required — your market slug (Settings → General → Details & markets)
);

await KindlySDK.displayChat();
```

### Authentication

```dart
await KindlySDK.start(
  botKey: 'YOUR_BOT_KEY',
  market: 'general',
  authTokenCallback: (chatId) async {
    // chatId is the backend-generated identifier for this chat session
    // (from /connect). Include it when calling your auth backend so the
    // JWT is scoped to this chat. Matches the Web / iOS / Android contract.
    final token = await yourAuthService.getToken(chatId: chatId);
    return token;
  },
);
```

Use `authCredentialsCallback` instead when your identity provider issues more than a single JWT: a
companion access token, an issuer, or an opaque token whose expiry only you know.

```dart
await KindlySDK.start(
  botKey: 'YOUR_BOT_KEY',
  market: 'general',
  authCredentialsCallback: (chatId) async {
    final tokens = await yourAuthService.getTokens(chatId: chatId);
    return KindlyAuthCredentials(
      idToken: tokens.idToken,                // optional
      accessToken: tokens.accessToken,        // optional
      issuer: 'https://idp.example.com/',     // optional
      expiresAt: tokens.expiresAtUnixSeconds, // optional, Unix seconds
    );
  },
);
```

Every field is optional, but set at least one of `idToken` / `accessToken`. Either token alone is a
valid credential: a provider that issues only an access token is normal and supported, and that
access token becomes the bearer. Credentials carrying neither are refused and the chat stays
anonymous. `KindlyAuthCredentials.bearerToken` tells you which token will be sent as
`Authorization: Bearer`, `idToken` first.

An opaque token that nobody can decode goes in `idToken` with `issuer` set, so the backend knows
which provider to introspect it against. The access token is only sent alongside an id token, as
`X-Access-Token`, and only when the two differ. A duplicate would fail the auth outright.

A single-JWT integration never needs this callback. When both callbacks are given,
`authCredentialsCallback` wins.

The SDK refreshes credentials on its own, 30 seconds before they expire, using whichever is earliest
of the backend's reported expiry, your `expiresAt`, and the `exp` claim of the bearer. If none is
known the credentials are treated as non-expiring, which is a trap for opaque tokens, so set
`expiresAt` there.

When the identity changes rather than the token, tell the SDK:

```dart
await KindlySDK.authenticate();     // user signed in, or switched account
await KindlySDK.deauthenticate();   // user signed out
```

`authenticate()` re-binds fresh credentials to the chat that is already open, keeping the
conversation. `deauthenticate()` unbinds the identity on the backend and clears the stored
credentials; the chat carries on anonymously with its history intact, and calling it again when the
chat is already anonymous does nothing.

### Custom Theme

```dart
await KindlySDK.setCustomTheme(
  background: Colors.white,
  botMessageBackground: Colors.grey[200],
  botMessageText: Colors.black,
  userMessageBackground: Colors.blue,
  userMessageText: Colors.white,
  buttonBackground: Colors.blue,
  buttonText: Colors.white,
  navBarBackground: Colors.blue,
  navBarText: Colors.white,
  inputBackground: Colors.grey[100],
  inputText: Colors.black,
);

// Revert to default
await KindlySDK.clearCustomTheme();
```

### Context Data

```dart
await KindlySDK.setNewContext({
  'userId': '12345',
  'userName': 'John Doe',
  'email': 'john@example.com',
  'plan': 'premium',
});

await KindlySDK.clearNewContext();
```

### Trigger Specific Dialogues

```dart
// Launch chat with a specific dialogue
await KindlySDK.launchChat(triggerDialogueId: 'welcome_dialogue');

// Or trigger a dialogue inside an active chat
await KindlySDK.triggerDialogue('help_dialogue');

// Clear any pending trigger (Android only)
await KindlySDK.clearTriggerDialogue();
```

### Deep links

The SDK recognises `kindly://chat/dialogue/{id}` URLs.

```dart
final handled = await KindlySDK.handleUrl('kindly://chat/dialogue/welcome');
```

### Programmatic Message Send

Send a message on behalf of the user. The SDK renders the bubble locally and reconciles the server-assigned id when the POST returns — do **not** also display the message yourself.

```dart
final reconciled = await KindlySDK.sendMessage(
  'Hello there',
  newContext: {'priority': 'high'},
);
print(reconciled?['id']); // server id
```

### Handover to a Human Agent

Initiate a handover and receive a callback when the handover begins.

```dart
await KindlySDK.callHandover(() {
  print('Handover initiated');
});
```

### Push Notifications

### iOS

```dart
await KindlySDK.setAPNSDeviceToken(deviceToken);
await KindlySDK.handleNotification(notificationData);
final isKindly = await KindlySDK.isKindlyNotification(notificationData);
```

### Android

```dart
await KindlySDK.saveNotificationToken(fcmToken);
await KindlySDK.handleNotification(remoteMessage.data);
final isKindly = await KindlySDK.isKindlyNotification(remoteMessage.data);
```

### Other Features

```dart
// Check chat UI state (iOS only)
final isDisplayed = await KindlySDK.isChatDisplayed;

// Update language for the active session
await KindlySDK.setLanguage('no');

// Save a JWT to the platform keychain (iOS only)
await KindlySDK.saveAuthToken('jwt-here');

// Toggle SDK logging / Sentry crash reporting
await KindlySDK.setVerboseLogging(true);
await KindlySDK.setCrashReporting(true);

// Close UI without ending the session
await KindlySDK.closeChat();

// End the active session (clears messages + token, dismisses UI)
await KindlySDK.endChat();

// Tear the SDK down completely — must call start() again before reuse
await KindlySDK.kill();
```

## API Reference

### Lifecycle

- **`start({botKey, market, languageCode?, authTokenCallback?, authCredentialsCallback?})`** — initialise the SDK. First call before anything else. `market` is **required** (Markets feature); the SDK loads that market's settings with no fallback. Use `authCredentialsCallback` for identity providers that issue more than a single JWT.
- **`displayChat({languageCode?, triggerDialogueId?})`** — show the chat UI.
- **`launchChat({triggerDialogueId?})`** — connect (if needed) and show the chat UI. Recommended on Android; routed to `displayChat` on iOS.
- **`closeChat()`** — dismiss the UI without ending the session.
- **`endChat()`** — end the active session, clear messages + token, dismiss UI.
- **`kill()`** — fully tear down the SDK; `start()` must be called again before reuse.
- **`setLanguage(languageCode)`** — change language for the current session.

### Authentication

- **`authenticate()`**: re-authenticate the live chat with fresh credentials, for a sign-in mid-session. Expiry is handled by the SDK on its own
- **`deauthenticate()`**: sign out of the chat without ending it. History is kept and the chat continues anonymously

### Messaging

- **`sendMessage(text, {newContext?})`** — send a message programmatically; resolves with `{id, text, created, sender, ...}`.
- **`setNewContext(context)`** — stage `Map<String, String>` context for the next message or click.
- **`clearNewContext()`** — clear staged context.
- **`triggerDialogue(dialogueId)`** — trigger a dialogue by id (must be connected).
- **`clearTriggerDialogue()`** — clear pending trigger (Android only).
- **`handleUrl(url)`** — handle `kindly://chat/dialogue/{id}` deep links; returns `bool`.
- **`callHandover(callback)`** — request handover to a human agent (Android only).

### Theme

- **`setCustomTheme({...colors})`** — override individual theme slots (`background`, `botMessageBackground`, `botMessageText`, `userMessageBackground`, `userMessageText`, `buttonBackground`, `buttonText`, `buttonOutline`, `buttonSelectedBackground`, `navBarBackground`, `navBarText`, `inputBackground`, `inputText`, `inputCursor`, `chatLogElements`, `defaultShadow`, `maintenanceHeaderBackground`).
- **`clearCustomTheme()`** — revert to the default / API theme.

### Push

- **`setAPNSDeviceToken(token)`** — register the APNS token (iOS only).
- **`saveNotificationToken(token)`** — register the FCM token (Android only).
- **`handleNotification(data)`** — forward a push to the SDK.
- **`isKindlyNotification(data)`** — `true` if the push originated from Kindly.

### State & settings

- **`isChatDisplayed`** — `Future<bool>`; iOS only (Android always returns `false` — track via host UI state).
- **`saveAuthToken(token)`** — persist a JWT to the platform keychain (iOS).
- **`setVerboseLogging(bool)`** — toggle verbose logging.
- **`setCrashReporting(bool)`** — toggle Sentry crash reporting.

## License

This project is licensed under the MIT License — see the LICENSE file for details.

***
