---
title: Authentication
slug: authentication
docTags: 
createdAt: 2026-09-14T23:41:45.358Z
---

## Setup

To use authentication with the Kindly SDK, you can initialize the SDK by adding an argument to the `start` method. Here's an example of how to set the `authTokenCallback` using the `start` method:

`KindlySDK.start(botKey: String, market: String, authTokenCallback: (_ chatId: String, _ promise: Promise<String>) -> Void)`

```swift
KindlySDK.start(botKey: "BOT_KEY", market: "YOUR_MARKET") { chatId, promise in
    // Generate JWT token
    // On success, call
    promise.fulfill("JWT_TOKEN")
    // On error
    promise.reject(ERROR)
}
```

Inside the `authTokenCallback` closure, you can generate a JWT token and fulfill the promise with the token on success. If there is an error, you can reject the promise with the appropriate error.

Additionally, there is another way to set the `authTokenCallback` using the `KindlySDK.config.getAuthToken` property. Here's an example of how to do it:

```swift
KindlySDK.start(botKey: "BOT_KEY", market: "YOUR_MARKET")
KindlySDK.config.getAuthToken = { chatId, promise in
    // Generate JWT token
    // On success, call
    promise.fulfill("JWT_TOKEN")
    // On error
    promise.reject(ERROR)
}
```

In this example, you set the `botKey` directly in the `start` method. Then, you assign a closure to the `KindlySDK.config.getAuthToken` property. Inside the closure, you can generate the JWT token and fulfill the promise with the token. You can also reject the promise with an error if needed.

Remember to replace `BOT_KEY` with your actual bot key.

## Manually Save Authentication Token

If you need to manually save an authentication token (for example, if you retrieve it from another source):

```swift
let success = KindlySDK.saveAuthToken("YOUR_JWT_TOKEN")
```

The method returns a boolean indicating whether the token was successfully saved to the keychain.

## Identity Providers That Issue More Than One Token

The Kindly platform verifies an authenticated chat in one of three ways: a JWT your own backend
signs, a JWT from your identity provider (optionally paired with an access token), or an opaque
bearer token validated by introspection.

The first is the single-string case above. For the other two, fulfil the promise with a
`KindlyAuthCredentials` instead:

`KindlySDK.start(botKey: String, market: String, authCredentialsCallback: (_ chatId: String, _ promise: Promise<KindlyAuthCredentials>) -> Void)`

```swift
KindlySDK.start(botKey: "BOT_KEY", market: "YOUR_MARKET") { chatId, promise in
    promise.fulfill(
        KindlyAuthCredentials(
            idToken: idToken,
            accessToken: accessToken,
            issuer: "https://idp.example.com/",
            expiresAtUnixSeconds: expiresAt
        )
    )
}
```

| Field         | Sent as                                                                                                      | When you need it                                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `idToken`     | `Authorization: Bearer`, when set                                                                            | The OIDC id token, if your provider issues one. For introspection this is the opaque token itself                                 |
| `accessToken` | `X-Access-Token` next to a different id token, or `Authorization: Bearer` when it is the only token you have | Dual-token provider setups, and providers that issue no id token at all. Ignored by the Kindly JWT strategy in its companion form |
| `issuer`      | `X-Auth-Issuer`                                                                                              | Bots with more than one provider configured, and any opaque token                                                                 |
| `expiresAt`   | not sent                                                                                                     | Opaque tokens, where there is no `exp` claim for the SDK to read                                                                  |

Every field is optional, but at least one of `idToken` / `accessToken` has to be set. Credentials
carrying neither cannot authenticate anything, and the chat stays anonymous.

### Which token becomes the bearer

`Authorization` is mandatory on Kindly's auth route, so whichever token you have goes there:
`idToken` first, falling back to `accessToken`. A provider that returns only an access token is a
normal, supported case rather than a workaround. That access token becomes the bearer, and Kindly
verifies it by whichever strategy the bot is configured for.

Three read-only accessors say what the SDK will actually send:

```swift
credentials.bearerToken           // idToken ?? accessToken, first non-empty. nil if neither is set
credentials.hasToken              // bearerToken != nil, the real "can this authenticate?" check
credentials.companionAccessToken  // the value that will be sent as X-Access-Token, or nil
```

`companionAccessToken` is the access token in its companion role only. It is `nil` when the access
token is itself the bearer, and `nil` when it merely duplicates the id token. Kindly verifies the
companion against the id token's `at_hash`, so sending a duplicate would fail an auth that would
otherwise have succeeded. The SDK drops it for you.

`X-Access-Token` and `X-Auth-Issuer` are read on `auth/chat`, `message` and `trigger` only.

A single-JWT integration never needs this type, so keep returning a string. The credentials callback
can also be set after `start()`:

```swift
KindlySDK.config.getAuthCredentials = { chatId, promise in /* … */ }
```

`getAuthCredentials` takes precedence over `getAuthToken` when both are set.

Credentials can also be saved directly, the same way a token can:

```swift
let success = KindlySDK.saveAuthCredentials(
    KindlyAuthCredentials(idToken: "YOUR_ID_TOKEN", accessToken: "YOUR_ACCESS_TOKEN")
)
```

## Token Refresh

The SDK refreshes credentials on its own, 30 seconds before they expire. Expiry can come from three
places, and whichever is earliest wins:

1. `expires_at` returned by the backend when it bound the credentials
2. `expiresAt` you supplied on `KindlyAuthCredentials`
3. The `exp` claim of the bearer token, when it is a JWT. That is the id token when you supplied one,
   and the access token when it is the only token you have

If none of the three is known the credentials are treated as non-expiring and never proactively
refreshed. That is correct for a JWT with no `exp` claim, but it is a trap for an opaque token: there
is no claim to read, so supply `expiresAt` or the SDK will keep using it until the backend rejects
it.

## Re-authenticating Mid-Session

Expiry is handled for you. These two are for when the identity itself changes, which the SDK cannot
detect:

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

`authenticate()` asks your callback for fresh credentials and re-binds them to the chat that is
already open, keeping the conversation and its history.

`deauthenticate()` unbinds the identity on the backend and clears the stored credentials. The chat
carries on anonymously with its history intact. Use `endChat()` instead when the conversation
itself should end. It is safe to call when the chat is already anonymous.

Both return a `Promise<Void>`.
