> ## Documentation Index
> Fetch the complete documentation index at: https://docs.karta.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Identity

> Three levels of end-user identity in the widget - anonymous, soft, and verified (HMAC) - and exactly how the verified scheme works.

The widget supports three levels of end-user identity. They differ in one thing:
**whether the server can trust who the user is.**

| Level         | How it is set                                    | Trusted by the server?                                         |
| ------------- | ------------------------------------------------ | -------------------------------------------------------------- |
| **Anonymous** | Nothing - the default                            | No. A per-browser visitor id is generated for continuity only. |
| **Soft**      | `karta("identify", { userId })` / `data-user-id` | No. Advisory metadata; any page can set any value.             |
| **Verified**  | `userId` + an HMAC `identityToken`               | Yes. The token proves your server vouches for this `userId`.   |

## Anonymous

With no identity set, the widget stores a random visitor id in `localStorage`
so a reload resumes the same conversation in the same browser. Nothing about the
user is asserted to the server.

## Soft (advisory) identity

Pass a `userId` to key reload continuity, so the same browser resumes the same
conversation:

```js theme={null}
karta("identify", { userId: "u_123" });
```

<Warning>
  **Soft `userId` is advisory and is never used for auth or credentials.** Any
  page can set any value, so the server does not bind it into the session token

  * it rides only as client-side session metadata. Do not use it to gate access
    to per-user data. For that, use verified identity below.
</Warning>

## Verified identity (HMAC)

Verified identity proves to Karta that **your** server vouches for this
`userId`, so the agent can safely act on per-user data. You sign the `userId`
with a per-key secret; the widget forwards the signature; Karta verifies it and
binds the verified subject into the short-lived session token.

### The scheme

For normal verified identity, compute the token on your **server** (never in
the browser - the secret must not ship to a client):

```
identity_token = HMAC-SHA256(identity_verification_secret, userId)   // lowercase hex
```

* The **secret** is the `identity_verification_secret` you generate on the
  agent's [Embed tab](/sdks/widget/theming). It is server-only and never
  shown in a browser.
* The **message** is the exact `userId` string you also pass to the widget.
* The output is **lowercase hexadecimal** (a SHA-256 HMAC, 64 hex chars).

### Server-side signing

<CodeGroup>
  ```js Node theme={null}
  import crypto from "node:crypto";

  function kartaIdentityToken(userId) {
    return crypto
      .createHmac("sha256", process.env.KARTA_IDENTITY_SECRET)
      .update(userId)
      .digest("hex"); // lowercase hex
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib, os

  def karta_identity_token(user_id: str) -> str:
      return hmac.new(
          os.environ["KARTA_IDENTITY_SECRET"].encode(),
          user_id.encode(),
          hashlib.sha256,
      ).hexdigest()  # lowercase hex
  ```
</CodeGroup>

Render the token into the page (it is per-user, short by nature, and safe to
expose to that user's browser - it only proves *that* user's id).

### Wiring it into the widget

Pass the `userId` and `identityToken` together - on the tag:

```html theme={null}
<script
  async
  src="https://cdn.karta.sh/widget/v1/karta.js"
  data-embed-key="pk_live_xxx"
  data-agent="coffeeco/support-bot"
  data-user-id="u_123"
  data-identity-token="<hmac-hex-from-your-server>"
></script>
```

or via the command API:

```js theme={null}
karta("identify", {
  userId: "u_123",
  identityToken: serverMintedHmacToken,
});
```

The widget forwards `user_id` + `identity_token` to the embed token mint
(`POST /v1/embed/session-tokens`). Karta recomputes the HMAC with the
constant-time compare and, on a match, mints a session token whose `sub` is
bound to that `userId`. A bad signature is rejected; a `userId` with no token
stays soft (never bound).

<Note>
  The `identityToken` is **not** an auth source on its own - the embed key (or
  session token) is the credential. The token only **upgrades** a soft `userId`
  to verified. Its validation is entirely server-side.
</Note>

## Telling the agent about your user

A verified identity says *who* the user is. A `context` bag says what the agent
should **know** about them, and Karta injects it into the agent's instructions
every turn.

```python theme={null}
import base64, hmac, hashlib, json, time, uuid

def karta_identity_token_with_context(user_id: str, context: dict) -> str:
    now = int(time.time())
    claims = {
        "user_id": user_id,
        "context": context,
        "iat": now,
        "exp": now + 3600,          # required - see below
        "jti": str(uuid.uuid4()),   # required - see below
    }
    payload = base64.urlsafe_b64encode(
        json.dumps(dict(sorted(claims.items())), separators=(",", ":")).encode()
    ).rstrip(b"=").decode()
    signature = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return f"v2.{payload}.{signature}"
```

```python theme={null}
karta_identity_token_with_context("u_123", {
    "userinfo": {"name": "Amol Kelkar", "email": "amol@shop.example"},
    "account":  {"tier": "enterprise", "seats": 40},
    "cart":     {"items": 3},
})
```

`userinfo` is the one reserved key: its `name` and `email` also drive the
widget's "Signed in as" line. Everything else is yours.

**A context-bearing token must expire.** A plain identity token has no
timestamp and stays valid indefinitely, which is fine for "this is user\_123" -
a fact you re-assert freely. It is not fine for a token that also asserts
*facts*: a captured one would keep vouching for a cancelled plan or a departed
employee. So `iat`, `exp` and `jti` are **required** whenever `context` is
present, and a token carrying context without them is rejected.

### Keeping a long conversation alive

Because the token now expires, the widget needs a way to get a new one. Give it
`identityTokenFn` and it will ask for a fresh proof on every mint:

```js theme={null}
karta("init", {
  embedKey: "pk_live_...",
  identity: { userId: "u_123" },
  identityTokenFn: async () => {
    const res = await fetch("/karta/identity-token");
    return (await res.json()).identityToken;
  },
});
```

Without it, the widget reuses the one token you passed at init - fine while that
token is valid, but once it lapses the conversation can no longer renew its
session and stops working. Set `exp` comfortably longer than your session-token
lifetime (default 15 minutes) either way, and under the one-hour ceiling: a
context-bearing proof whose `exp - iat` exceeds 3600 seconds is rejected.

If your endpoint is briefly down, the widget falls back to the token you passed
at init rather than failing: losing the context is recoverable, losing the
session is not.

<Note>
  Put in `context` only what the agent may see and what you accept being
  stored with the conversation. Prefer facts your server derived over free text
  your user typed: the agent reads this like any other input, so treat it as
  data you are showing the model, not as instructions it will obey.
</Note>

### Turning it on for an agent

Injection is **off by default** - it costs tokens on every turn, so it is the
agent's choice. Opt in from `karta.toml`:

```toml theme={null}
[end_user_context]
enabled = true
```

Without this the bag is still verified and still drives the sign-in status; it
just never reaches the agent's prompt.

Limits: 40KB of JSON, 200 keys, 6 levels deep, 4KB per string, 100 items per
array, and roughly 10K tokens once rendered. Over any of them the bag is
**rejected, not trimmed** - a truncated bag could drop a "not" and leave the
agent believing the opposite of what you said.

<Note>
  Context needs a session to attach to, so it works on the **Managed Agents**
  and **Responses** transports. The **Chat Completions** adapter is stateless -
  it runs each call on a fresh throwaway session - so context is not supported
  there. It would apply to your first message and silently vanish from the
  second, which is worse than not offering it.
</Note>

Context is captured when the session starts and stays fixed for that
conversation. If a user upgrades mid-chat, the agent sees it in their next
conversation, not this one.

## Host-attested step-up

If your app has completed a stronger verification step for the user, your
server can mint a structured identity token that carries that fact with the
same identity signature. Karta uses this for sensitive approval flows: the
browser cannot add `stepped_up_at` or `aal` next to a regular HMAC and have it
trusted.

The structured token format is:

```
v2.<base64url-json-payload>.<hmac-hex>
```

The JSON payload must include the same `user_id` passed to the widget and may
include:

| Field           | Meaning                                                       |
| --------------- | ------------------------------------------------------------- |
| `stepped_up_at` | Unix timestamp, seconds, for when your app completed step-up. |
| `aal`           | Authentication assurance label, such as `mfa`.                |

The HMAC signs the encoded payload segment:

```js theme={null}
import crypto from "node:crypto";

function stepUpIdentityToken(userId) {
  const payload = Buffer.from(JSON.stringify({
    user_id: userId,
    stepped_up_at: Math.floor(Date.now() / 1000),
    aal: "mfa",
  })).toString("base64url");

  const signature = crypto
    .createHmac("sha256", process.env.KARTA_IDENTITY_SECRET)
    .update(payload)
    .digest("hex");

  return `v2.${payload}.${signature}`;
}
```

After the user completes your step-up challenge, call `identify` again with the
new token:

```js theme={null}
karta("identify", {
  userId: "u_123",
  identityToken: serverMintedStepUpToken,
});
```

Changing `identityToken` invalidates the widget's cached session token, so the
next widget request is minted with the fresh signed claims. See the
[authenticated agent recipe](/sdks/widget/authenticated-agent) for the full
flow.

## Backend-minted tokens (alternative)

If you already mint session tokens server-side with a `kt_live_...` API key, skip
the embed key entirely and hand the widget a token function or endpoint. Your
backend holds the secret key; the browser only ever sees the short-lived token.

```js theme={null}
karta("init", {
  agentRef: "coffeeco/support-bot",
  tokenEndpoint: "/api/karta-token", // your endpoint returns { token }
});
```

Your endpoint calls
`POST https://karta.sh/api/agents/:slug/session_tokens` with your
`kt_live_...` key and returns the minted token. Because Karta authenticated that
server key, the subject you put on the token is authoritative without an HMAC.
See [Session tokens](/security/session-tokens).

The headless client also accepts a refreshable `tokenFn` (re-called with
`{ force: true }` after a `401`) - see [Headless & React](/sdks/widget/headless-react).

## Next

<CardGroup cols={2}>
  <Card title="Theming & config" icon="palette" href="/sdks/widget/theming">
    Generate and rotate the identity secret on the Embed tab.
  </Card>

  <Card title="Security & privacy" icon="shield-halved" href="/security/chat-widget">
    The credential boundaries and the data path.
  </Card>

  <Card title="Authenticated agent" icon="user-lock" href="/sdks/widget/authenticated-agent">
    A worked server-side recipe for signed-in users and step-up.
  </Card>
</CardGroup>
