---
title: Authentication
slug: web-view-sdk-authentication
createdAt: 2026-11-03T10:14:17.037Z
---

An authenticated chat knows which of your users it talks to. Pass an auth provider to `start`, and the SDK asks it for a token whenever the chat needs one: after the chat connects, when you call `authenticate()`, and within 30 seconds of the previous token's expiry

The provider gets the chat id and returns a JWT, or an opaque bearer token, for the user signed in to your app. The chat id comes from the chat page and the SDK cannot verify it, so issue the token only for the user signed in to your app, and never treat the chat id as proof of who the user is or what they may see. Tokens come from your backend: never put a signing key or another long-lived secret in the app

## Token provider

::::Tabs
:::Tab{title="iOS"}
```swift
KindlyWebViewSDK.start(botKey: "YOUR_BOT_KEY", market: "general", authTokenProvider: { chatId in
    try await MyBackend.kindlyToken(chatId: chatId)
})
```

Providers are `@Sendable` closures, so read shared state through an actor or another thread-safe type
:::

:::Tab{title="Android"}
```kotlin
KindlyWebViewSDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    market = "general",
    authTokenProvider = KindlyAuthTokenProvider { request ->
        myBackend.kindlyToken(request.chatId)
    },
)
```

Providers are `suspend` functions that the SDK calls from a background coroutine. `request.reason` is `AuthTokenRequest.Reason.INITIAL_CONNECT` or `AuthTokenRequest.Reason.EXPIRED`
:::

:::Tab{title="Flutter"}
```dart
await KindlyWebViewSDK.start(
  botKey: 'YOUR_BOT_KEY',
  market: 'general',
  authTokenCallback: (String chatId) => myBackend.kindlyToken(chatId),
);
```

Return `null` to keep the chat anonymous
:::

:::Tab{title="React Native"}
```typescript
await KindlyWebViewSDK.start({
  botKey: 'YOUR_BOT_KEY',
  market: 'general',
  authTokenCallback: async (chatId) => fetchKindlyToken(chatId),
});
```

Return `null` to keep the chat anonymous
:::
::::

## Credentials provider

When your identity provider issues more than one token, return credentials instead of a token. The Kindly platform verifies a chat's credentials the way the bot is configured: a JWT your backend signs, a JWT from your identity provider with or without its access token, or an opaque token it introspects

::::Tabs
:::Tab{title="iOS"}
```swift
KindlyWebViewSDK.start(botKey: "YOUR_BOT_KEY", market: "general", 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, a token provider or a credentials provider. `expiresAt` is a `Date`, and `KindlyAuthCredentials(idToken:accessToken:issuer:expiresAtUnixSeconds:)` takes Unix seconds instead
:::

:::Tab{title="Android"}
```kotlin
KindlyWebViewSDK.start(
    application = this,
    botKey = "YOUR_BOT_KEY",
    market = "general",
    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 are set. `expiresAt` is Unix seconds
:::

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

The credentials callback wins when both are given. `expiresAt` is Unix seconds
:::

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

The credentials callback wins when both are given. `expiresAt` is Unix seconds
:::
::::

Credentials need at least one of `idToken` and `accessToken`. The SDK sends the `idToken` as the bearer token when there is one and the `accessToken` otherwise, so a provider that issues only an access token works, and next to an `idToken` a different `accessToken` goes along as its companion. Credentials with neither leave the chat anonymous, and the Flutter and React Native packages log one warning

Set `issuer` for opaque tokens and for bots with more than one identity provider. Set `expiresAt` when the expiry cannot be read from the token, as with an opaque token, because credentials without a known expiry are never refreshed ahead of time. An `expiresAt` above 1e12, a millisecond value, is converted to seconds

## Timeouts and failures

The SDK waits 15 seconds for a provider. If it throws, returns an empty token or does not answer in time, the chat continues anonymously and a late answer is discarded. The Flutter and React Native packages stop waiting for your callback after 12 seconds (`KindlyWebViewSDK.authCallbackTimeout` in Flutter, `KindlyWebViewSDK.authCallbackTimeoutMs` in React Native), so their answer always reaches the native SDK in time, and a callback that returns `null` leaves 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 and 4-second limits nor a `setTimeout` in your callback fires until the chat is hidden. The native SDK's bounds apply instead: after 15 seconds without a token the chat continues anonymously, and after 5 seconds without a [Link Interception](docId:1D_RWHC_US3g18s-OBWW4) the link opens outside the app. `fetch` and promises keep working while the chat is shown

The SDK calls the provider at most six times a minute and runs one call at a time per chat. A token must be printable ASCII without spaces, at most 16 KiB, and an issuer printable ASCII of at most 2,048 characters. Credentials that break these rules leave the chat anonymous

## Sign in and sign out

::::Tabs
:::Tab{title="iOS"}
```swift
try await KindlyWebViewSDK.authenticate()    // after the user signs in
try await KindlyWebViewSDK.deauthenticate()  // after the user signs out
```

`authenticate()` throws `KindlyWebViewError.noAuthProvider` when `start` got no provider
:::

:::Tab{title="Android"}
```kotlin
lifecycleScope.launch {
    KindlyWebViewSDK.authenticate().await()    // after the user signs in
    KindlyWebViewSDK.deauthenticate().await()  // after the user signs out
}
```

`authenticate()` fails with `KindlyWebViewException.NoAuthProvider` when `start` got no provider
:::

:::Tab{title="Flutter"}
```dart
await KindlyWebViewSDK.authenticate();    // after the user signs in
await KindlyWebViewSDK.deauthenticate();  // after the user signs out
```

`authenticate()` fails with `nativeError` when `start` got no auth callback
:::

:::Tab{title="React Native"}
```typescript
await KindlyWebViewSDK.authenticate();    // after the user signs in
await KindlyWebViewSDK.deauthenticate();  // after the user signs out
```

`authenticate()` rejects with `NATIVE_ERROR` when `start` got no auth callback
:::
::::

`authenticate()` makes the loaded chat authenticate again with your provider and keeps the conversation, and without a loaded page the next page authenticates as it loads. Use it when a user signs in while the chat runs anonymously. After `deauthenticate()` the chat continues anonymously and the SDK refuses token requests until the next `authenticate()`

Called 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, see [Errors](docId\:RRLh0LP2LvUdYOYKVEyLB)

`deauthenticate()` keeps the conversation. When a user signs out of your app, also call [End Chat](docId\:uCgQWla-zeq6uPJDBKGUX), which deletes the stored conversation
