---
title: Kindly Web View SDK, Authentication Reference
slug: sdk/web-view-sdk-skill-auth
description: Reference for authenticated chats in the Kindly web view SDK on iOS, Android, Flutter and React Native. Use when wiring a token or credentials provider (authTokenProvider, authCredentialsProvider, authTokenCallback, authCredentialsCallback), returning…
createdAt: 2026-11-04T11:39:36.349Z
---

How an app tells the chat who its user is, on all four platforms. Companion to [SKILL.md](https://docs.kindly.ai/sdk/web-view-sdk-skill.md). The guide page is [Authentication](https://docs.kindly.ai/sdk/web-view-sdk-authentication)

The app gives `start` a provider. Whenever the chat needs credentials, the SDK calls it with the chat id and the provider returns short-lived credentials for the user signed in to the app. The Kindly platform verifies them in one of three ways, set per bot:

| Strategy          | What the provider returns                                                                         | Verified by                                 |
| ----------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| Kindly JWT        | A JWT the app's backend signs                                                                     | The public key held by Kindly               |
| Identity provider | A JWT from the identity provider, alone or with its access token. An access token alone works too | The identity provider's JWKS                |
| Introspection     | An opaque bearer token, with its issuer                                                           | Introspection against the identity provider |

The first is one string, so a token provider is enough. The other two can need more than one value, which is what the credentials provider is for

***

## When the SDK asks

- After the chat connects, for a new or a restored conversation
- When the app calls `authenticate()`
- When the current credentials are within 30 seconds of expiry

The chat id comes from the chat page and the SDK cannot verify it. Issue credentials only for the user signed in to the app, and never treat the chat id as proof of who the user is or what they may see. Credentials come from the app's backend or identity provider: never put a signing key or another long-lived secret in the app

***

## Token provider

### iOS, token provider

```swift
import KindlyWebView

KindlyWebViewSDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET", authTokenProvider: { chatId in
    try await MyBackend.kindlyToken(chatId: chatId)
})
```

The type is `KindlyWebViewAuthTokenProvider`, `@Sendable (String) async throws -> String`. Read shared state through an actor or another thread-safe type

### Android, token provider

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

KindlyWebViewSDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    market = "YOUR_MARKET",
    authTokenProvider = KindlyAuthTokenProvider { request ->
        myBackend.kindlyToken(request.chatId)
    },
)
```

`fetchToken` is a `suspend` function that the SDK calls from a background coroutine. `request` is an `AuthTokenRequest` with `chatId` and `reason`, `AuthTokenRequest.Reason.INITIAL_CONNECT` or `AuthTokenRequest.Reason.EXPIRED`

### Flutter, token provider

```dart
await KindlyWebViewSDK.start(
  botKey: 'YOUR_BOT_KEY',
  market: 'YOUR_MARKET',
  authTokenCallback: (String chatId) => myBackend.kindlyToken(chatId),
);
```

The type is `KindlyWebViewAuthTokenCallback`, `Future<String?> Function(String chatId)`. Return `null` to keep the chat anonymous

### React Native, token provider

```typescript
await KindlyWebViewSDK.start({
  botKey: 'YOUR_BOT_KEY',
  market: 'YOUR_MARKET',
  authTokenCallback: async (chatId) => fetchKindlyToken(chatId),
});
```

The callback returns a string, `null` or `undefined`, or a promise of one. `null` and `undefined` keep the chat anonymous

`MyBackend`, `myBackend` and `fetchKindlyToken` stand for the app's own call to its backend

***

## Credentials provider

Use it when the identity provider issues more than one token, only an access token, or an opaque token whose expiry only the app knows

### iOS, credentials provider

```swift
KindlyWebViewSDK.start(botKey: "YOUR_BOT_KEY", market: "YOUR_MARKET", authCredentialsProvider: { chatId in
    let tokens = try await MyIdentityProvider.tokens()
    return KindlyAuthCredentials(
        idToken: tokens.idToken,
        accessToken: tokens.accessToken,
        issuer: "https://idp.example.com",
        expiresAt: tokens.expiry
    )
})
```

`start` takes one provider, so this is a separate overload. `expiresAt` is a `Date`; `KindlyAuthCredentials(idToken:accessToken:issuer:expiresAtUnixSeconds:)` takes Unix seconds instead

### Android, credentials provider

```kotlin
import ai.kindly.webview.KindlyAuthCredentials
import ai.kindly.webview.KindlyAuthCredentialsProvider

KindlyWebViewSDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    market = "YOUR_MARKET",
    authCredentialsProvider = KindlyAuthCredentialsProvider { request ->
        val tokens = myIdentityProvider.tokens()
        KindlyAuthCredentials(
            idToken = tokens.idToken,
            accessToken = tokens.accessToken,
            issuer = "https://idp.example.com",
            expiresAt = tokens.expiresAtUnixSeconds,
        )
    },
)
```

The credentials provider wins when both providers are set. `expiresAt` is a `Long` of Unix seconds

### Flutter, credentials provider

```dart
await KindlyWebViewSDK.start(
  botKey: 'YOUR_BOT_KEY',
  market: 'YOUR_MARKET',
  authCredentialsCallback: (String chatId) async => KindlyWebViewAuthCredentials(
    idToken: idToken,
    accessToken: accessToken,
    issuer: 'https://idp.example.com',
    expiresAt: expiryInUnixSeconds,
  ),
);
```

The credentials callback wins when both callbacks are given. `expiresAt` is an `int` of Unix seconds, and `toString()` never prints a token

### React Native, credentials provider

```typescript
await KindlyWebViewSDK.start({
  botKey: 'YOUR_BOT_KEY',
  market: 'YOUR_MARKET',
  authCredentialsCallback: async (chatId) => ({
    idToken,
    accessToken,
    issuer: 'https://idp.example.com',
    expiresAt: expiryInUnixSeconds,
  }),
});
```

The value has the shape `KindlyWebViewAuthCredentials`. The credentials callback wins when both callbacks are given. `expiresAt` is a number of Unix seconds

### Which token becomes the bearer

Credentials need at least one of `idToken` and `accessToken`:

| `idToken` | `accessToken`  | What the SDK sends                                                              |
| --------- | -------------- | ------------------------------------------------------------------------------- |
| Set       | Not set        | The `idToken` as the bearer                                                     |
| Not set   | Set            | The `accessToken` as the bearer                                                 |
| Set       | Set, different | The `idToken` as the bearer and the `accessToken` as its companion token        |
| Set       | Set, the same  | The `idToken` as the bearer, without a companion                                |
| Not set   | Not set        | Nothing. The chat stays anonymous, and Flutter and React Native log one warning |

An empty string counts as not set. The backend checks a companion token against the id token's `at_hash` and rejects a pair that does not match, so send the access token from the same token response as the id token. On iOS and Android, `bearerToken`, `companionAccessToken` and `hasToken` on `KindlyAuthCredentials` show what the SDK will send

### Issuer and expiry

- Set `issuer` for opaque tokens and for bots with more than one identity provider. The backend cannot read the issuer out of a token it cannot decode
- Set `expiresAt` when the expiry cannot be read from the token, as with an opaque token. Credentials without a known expiry are never refreshed ahead of time
- An `expiresAt` above 1e12 is a millisecond value and is converted to seconds, on every platform

***

## Rules the SDK applies

| Rule                                           | Value                                                                                                                   |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Native wait for a provider                     | 15 seconds, then the chat continues anonymously and a late answer is discarded                                          |
| Flutter and React Native wait for the callback | 12 seconds: `KindlyWebViewSDK.authCallbackTimeout` in Flutter, `KindlyWebViewSDK.authCallbackTimeoutMs` in React Native |
| Calls per minute                               | At most six                                                                                                             |
| Calls at a time                                | One per chat id. Requests for the same chat id while one runs share its answer                                          |
| Token                                          | Visible ASCII without spaces, at most 16 KiB. The same for the access token                                             |
| Issuer                                         | Printable ASCII, at most 2,048 characters                                                                               |
| Thrown error, empty token, `null`              | The chat continues anonymously                                                                                          |

Credentials that break the token or issuer rules leave the chat anonymous too

On Android, React Native pauses JavaScript timers while the chat screen is in front, so neither the package's own 12-second limit nor a `setTimeout` in the callback fires until the chat is hidden. The native 15 seconds apply there. `fetch` and promises keep working while the chat is shown

***

## Signing in and out

### iOS, sign in and out

```swift
try await KindlyWebViewSDK.authenticate()    // after the user signs in
try await KindlyWebViewSDK.deauthenticate()  // after the user signs out
```

### Android, sign in and out

```kotlin
lifecycleScope.launch {
    KindlyWebViewSDK.authenticate().await()    // after the user signs in
    KindlyWebViewSDK.deauthenticate().await()  // after the user signs out
}
```

### Flutter and React Native, sign in and out

```dart
await KindlyWebViewSDK.authenticate();    // after the user signs in
await KindlyWebViewSDK.deauthenticate();  // after the user signs out
```

The React Native calls are the same, and return promises

What they do:

- `authenticate()` makes a loaded chat authenticate again with the provider and keeps the conversation. Without a loaded page, the next page authenticates as it loads. It also lifts a `deauthenticate()`
- `deauthenticate()` makes the chat anonymous and keeps the conversation. The SDK refuses token requests, without calling the provider, until the next `authenticate()`, and new chat pages load without authentication
- While the chat page is still loading, both take effect once it has loaded, in call order. If that page fails to load, they fail with the chat-not-ready error
- `authenticate()` without a provider fails: `KindlyWebViewError.noAuthProvider` on iOS, `KindlyWebViewException.NoAuthProvider` on Android, `nativeError` in Flutter and `NATIVE_ERROR` in React Native

When a user signs out of the app, call `endChat()` as well. `deauthenticate()` keeps the conversation, and `endChat()` deletes it from the device

***

## Changing the provider

- `start` with the same bot key and market replaces the provider and keeps the live chat. A call without a provider removes it, so pass the provider on every `start` call
- `start` with another bot key or market closes the chat and starts over with the provider given
- `kill()` forgets the provider. Pass it to the next `start` again

Flutter and React Native answer from one place:

- Flutter: the callbacks reach the Flutter engine whose Dart code last called `start`. An engine that never calls `start`, such as the background engine a messaging or work manager plugin creates, never receives them. After a hot restart, call `start` again. When the engine that called `start` goes away, the chat gets no token until another engine calls `start`
- React Native: the package answers from the JavaScript runtime that is running. After a reload, call `start` again, as the app's startup code normally does. Until then the chat gets no token

***

## End-to-end pattern

1. The app's backend exposes an endpoint that returns Kindly credentials for the signed-in user, never for an arbitrary chat id
2. The app passes a provider that calls that endpoint to every `start`
3. The provider throws, or returns `null` in Flutter and React Native, while nobody is signed in, so the chat runs anonymously
4. After sign-in, the app calls `authenticate()`
5. After sign-out, the app calls `deauthenticate()` and `endChat()`

***

## Debugging

Turn on the debug logs (`KindlyWebViewSDK.verboseLogging = true` on iOS and Android, `setVerboseLogging(true)` in Flutter and React Native). Each token request then logs its outcome code, never the token: on iOS under the subsystem `ai.kindly.webview`, on Android under the Logcat tag `KindlyWebView`

| Outcome code              | Meaning                                                                       |
| ------------------------- | ----------------------------------------------------------------------------- |
| `no_provider`             | `start` got no provider                                                       |
| `suspended`               | `deauthenticate()` ran, and `authenticate()` has not since                    |
| `provider_threw`          | The provider threw                                                            |
| `provider_returned_empty` | The provider returned an empty token, or credentials without a token          |
| `provider_timeout`        | The provider took longer than 15 seconds                                      |
| `invalid_credentials`     | A token or the issuer broke the rules above                                   |
| `invalid_chat_id`         | The chat page asked for a chat id that is empty or longer than 128 characters |
| `rate_limited`            | More than six requests in a minute                                            |

| Symptom                                                                                                      | Likely cause                                                                                                                |
| ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| The chat stays anonymous and the log says `no_provider`                                                      | `start` got no provider, or a later `start` call left it out                                                                |
| `provider_timeout`                                                                                           | The backend call takes too long, or in React Native on Android the callback waits on a `setTimeout` while the chat is shown |
| The chat stays anonymous in Flutter after a hot restart, or in React Native after a reload                   | `start` has not run again in the new runtime                                                                                |
| The chat stays anonymous although the credentials callback answered, and Flutter or React Native warned once | The credentials carry neither `idToken` nor `accessToken`                                                                   |
| Authentication works with the id token alone and fails with the access token added                           | The two tokens do not come from the same token response                                                                     |
| An opaque token is never refreshed                                                                           | Set `expiresAt`                                                                                                             |
| `authenticate()` fails with the no-provider error                                                            | `start` got no provider                                                                                                     |
| The chat stays anonymous after the user signs in                                                             | `authenticate()` was not called after sign-in                                                                               |

Kindly's backend answers the credentials themselves. When the provider returns a token and the chat still is not authenticated, check the bot's authentication settings in the Kindly platform against the token's issuer, audience and signing key

***

## Security notes

- Mint short-lived credentials on the backend. Never put a signing key or another long-lived secret in the app
- Scope every token to the user signed in to the app. The chat id the provider receives comes from the chat page, which can name any chat id
- The SDK never logs or stores a token. The chat page keeps its own session for `chat.kindlycdn.com` in the app's web view storage, per bot and market, until `endChat()` or `kill()` deletes it
- Script in the chat page can read the credentials the SDK hands the widget, as on any website that embeds the chat
