# Customer identity

> Log chat customers in with a JWT signed by your server so their conversations follow them across devices. Getting the signing secret, building the token, calling login on each SDK, describing customers, and logging out.

Source: https://developer.kustomer.com/docs/chat/identity

Chat starts anonymous: Kustomer knows the browser or device, not the person. When your user signs in to your site or app, log them in to chat too, so Kustomer attaches the conversation to their customer profile and shows them the same conversations on every device.

_Step-by-step diagram of chat identity between your app, your server and Kustomer. An anonymous visitor chats, so Kustomer creates an anonymous customer holding the conversation. After the user signs in, the app checks isLoggedIn, asks your server for a token, and the server signs an HS256 JWT with the externalId (or email) and iat claims using the chat secret. The app calls Kustomer.login({ jwtToken }); Kustomer verifies the signature and that iat is under 15 minutes old, finds or creates the customer with that externalId, and the conversation moves onto that customer, returning { identified, customerId, email, externalId } and firing onLogin. Finally, logout clears the session and cached conversations from the browser while the conversations stay on the customer in Kustomer._

The token is signed with a secret only your server holds, so a visitor can't claim to be someone else, even though the chat key in your page is public.

## 1. Get the chat secret

The secret comes from an admin-only endpoint. Call it once and store the result in your server's secrets manager:

`GET /v1/auth/customer/settings` ([reference](https://developer.kustomer.com/api/access-management/get-auth-customer-settings))

```bash title="cURL"
curl "https://acme.api.kustomerapp.com/v1/auth/customer/settings" \
  -H "Authorization: Bearer $KUSTOMER_ADMIN_KEY"
```

The key needs the `org.admin.apps` role, which `org.admin` includes. The secret is in the response at `data.attributes.secret`.

> **The secret never leaves your server:** Anyone with the secret can log in to chat as any of your customers and read their conversations. Keep it out of web pages, mobile apps and source control.

## 2. Sign a token

The JWT is signed with HS256 and has two claims:

| Claim                   | Value                                                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `externalId` or `email` | Who the user is. `externalId` is your own user ID, stored on the customer as `externalId`. If you send both, `email` wins. |
| `iat`                   | Issued-at time in seconds. Kustomer rejects tokens issued more than **15 minutes** before they're used.                    |

```js title="Node.js"
import jwt from 'jsonwebtoken'

export function kustomerChatToken(user) {
  return jwt.sign(
    { externalId: user.id, iat: Math.floor(Date.now() / 1000) },
    process.env.KUSTOMER_CHAT_SECRET,
    { algorithm: 'HS256', header: { alg: 'HS256', typ: 'JWT' } },
  )
}
```

```python title="Python"
import os, time, jwt  # PyJWT

def kustomer_chat_token(user):
    return jwt.encode(
        {"externalId": user.id, "iat": int(time.time())},
        os.environ["KUSTOMER_CHAT_SECRET"],
        algorithm="HS256",
    )
```

```ruby title="Ruby"
require "jwt"

def kustomer_chat_token(user)
  JWT.encode({ externalId: user.id, iat: Time.now.to_i }, ENV.fetch("KUSTOMER_CHAT_SECRET"), "HS256")
end
```

```go title="Go"
import (
	"os"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func KustomerChatToken(userID string) (string, error) {
	t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"externalId": userID,
		"iat":        time.Now().Unix(),
	})
	return t.SignedString([]byte(os.Getenv("KUSTOMER_CHAT_SECRET")))
}
```

Serve it from an authenticated endpoint, for example `POST /api/kustomer-token`, and mint a fresh token each time: the 15-minute window means you can't cache them. A complete Express example is in [`kustomer/web-chat-sdk-samples`](https://github.com/kustomer/web-chat-sdk-samples).

> **email or externalId?:** Prefer `externalId` when your users can change their email. Pick one and use it consistently: `isLoggedIn` checks against the same field you signed.

## 3. Log in

Call `login` right after your user signs in, and skip it when `isLoggedIn` says they already are.

**Web**

```js
async function identifyChat(user) {
  Kustomer.isLoggedIn({ externalId: user.id }, async function (loggedIn) {
    if (loggedIn) return
    const { token } = await fetch('/api/kustomer-token', { method: 'POST' }).then((r) => r.json())
    Kustomer.login({ jwtToken: token }, function (res, error) {
      if (error) return console.error(error)
      console.log('Chat identified as', res.customerId)
    })
  })
}
```

The response is `{ identified, customerId, email, externalId }`, and the widget fires `onLogin`. With Core, use `KustomerCore.login` and `KustomerCore.isLoggedIn` the same way.

**iOS**

```swift
if !Kustomer.isLoggedIn(userEmail: nil, userId: user.id) {
  Kustomer.logIn(jwt: tokenFromServer) { result in
    if case .failure(let error) = result { print("Login failed: \(error)") }
  }
}
```

The identity is kept in the keychain across launches. A successful login clears the local conversation cache and rebuilds the chat UI.

**Android**

```kotlin
val kustomer = Kustomer.getInstance()
if (kustomer.isLoggedIn(userId = user.id).dataOrNull != true) {
  kustomer.logIn(tokenFromServer) { result ->
    if (result is KusResult.Error) Log.e("Kustomer", "Login failed", result.exception)
  }
}
```

`logIn` also has a `suspend` overload. `isLoggedIn(userEmail, userId)` is synchronous.

**React Native**

```ts
if (!(await KustomerChat.isLoggedIn('userId', user.id))) {
  await KustomerChat.logIn(tokenFromServer)
}
```

> **Log in before the first message:** Identify the customer as soon as your app knows who they are, ideally before they open chat, so every conversation lands on their profile from the start. If Kustomer later merges customers or conversations, the SDKs tell you with merge events (`onConversationMerged`, and `onCustomerMerged` on Android).

## Describe the customer

Once a customer is identified, add details agents should see. These write to the customer's profile in Kustomer.

**Web**

```js
Kustomer.describeCustomer(
  {
    attributes: { sharedPhones: ['+15555550123'] },
    customAttributes: { loyaltyTierStr: 'gold', lifetimeValueNum: 1840 },
  },
  function (res, error) {},
)
```

**iOS**

```swift
Kustomer.chatProvider.describeCurrentCustomer(
  sharedPhones: ["+15555550123"],
  custom: ["loyaltyTierStr": "gold", "lifetimeValueNum": 1840]
)
```

**Android**

```kotlin
Kustomer.getInstance().describeCustomer(
  KusCustomerDescribeAttributes(
    sharedPhones = listOf(KusPhone("+15555550123")),
    custom = mapOf("loyaltyTierStr" to "gold", "lifetimeValueNum" to 1840),
  ),
) { }
```

**React Native**

```ts
await KustomerChat.describeCustomer(
  { sharedPhones: ['+15555550123'] },
  { loyaltyTierStr: 'gold', lifetimeValueNum: 1840 },
)
```

| Field                                           | Meaning                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sharedEmails`, `sharedPhones`, `sharedSocials` | Contact details to add. (The older `emails`, `phones` and `socials` still work but are deprecated.)                                                                                                                                                                  |
| Custom attributes                               | Use attribute **names** with their type suffix, such as `loyaltyTierStr` or `lifetimeValueNum`, not display names. The attributes must already exist on the customer Klass. See [Custom attributes](https://developer.kustomer.com/docs/concepts/custom-attributes). |

To set attributes on a conversation instead, use `describeConversation` with the conversation ID.

> **Don't identify with describeCustomer:** `describeCustomer` adds details to whoever the session belongs to. It doesn't prove who that is. Only a signed `login` token identifies a customer.

## Log out

Log out of chat whenever your user signs out, so the next person on the device starts fresh:

| Platform     | Call                                                      |
| ------------ | --------------------------------------------------------- |
| Web          | `Kustomer.logout(cb)` or `KustomerCore.logout(cb)`        |
| iOS          | `Kustomer.logOut { error in }` (also deregisters push)    |
| Android      | `deregisterDeviceForPushNotifications()`, then `logOut()` |
| React Native | `await KustomerChat.logOut()` (also deregisters push)     |

Logging out clears the chat session and cached conversations from the device. It doesn't end or delete conversations in Kustomer.

## Troubleshooting

| Symptom                                      | Cause                                                                                                                      |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Login fails with a valid-looking token       | `iat` is more than 15 minutes old, or your server's clock is wrong. Mint tokens on demand and sync the clock.              |
| Login fails with a fresh token               | It's signed with the wrong secret. Fetch it again from `/v1/auth/customer/settings`, and check you're using the right org. |
| `isLoggedIn` is always false                 | You're checking a different field than you signed: `{ email }` versus `{ externalId }`.                                    |
| Customer has a different email than expected | The token had both `email` and `externalId`, and `email` took precedence.                                                  |
