# End users and identity

> Who the agent is talking to — anonymous visitors, users your server vouches for, and short-lived client tokens — and what verification unlocks.

An **end user** (`eu_…`) is the person talking to your agent — your customer. End users belong to a project and carry your own `external_id`, an optional `name` and free-form `traits` (plan, city, language, account tier…). Sessions, the agent's action memory and per-user limits all hang off the end user.

## Three identity tiers

| Tier | How it happens | Verified | Typical use |
|---|---|---|---|
| **Anonymous** | A publishable key plus a device token, from the widget | No | Visitors on your public website |
| **Server-asserted** | A secret-key call that includes `end_user` | Yes | Your backend talking to K-Agent |
| **Client token** | A `ct_…` token minted by your server for one end user and one agent | Yes, for up to 60 minutes | Signed-in users in your web or mobile app |

**Verification is a property of the credential, not of the end user.** A request is verified when your server vouched for it — with a secret key or with a client token it minted. The same person chatting anonymously in the widget is not verified in that conversation.

### What verification unlocks

- **Identity-bound HTTP tools.** A tool with `requires_verified_user`, or one that uses `{{end_user.*}}` placeholders, only runs for verified users. The model never supplies identity: K-Agent fills it from the credential. See [HTTP tools](/docs/en/guides/http-tools/).
- **Actions for known people.** Tools with side effects are removed for anonymous users unless the agent sets `tools.allow_anonymous_actions`.
- **Memory across sessions.** The action ledger ("already done, don't repeat") and per-user rate limits follow a verified end user across all their sessions. For anonymous users they are scoped to the session.

## Tell K-Agent who the user is

From your server, pass `end_user` on `ask` or `POST /v1/sessions`:

```json
{
  "agent": "store-assistant",
  "external_id": "order-8812",
  "end_user": {
    "external_id": "cus_1042",
    "name": "Fahad",
    "traits": { "plan": "gold", "city": "Riyadh" }
  },
  "input": "Can I change the delivery address?"
}
```

- The end user is created on first sight and updated later (traits merge).
- A session's end user is fixed when the session is created; a different one returns `409 session_end_user_mismatch`.
- End-user `external_id`s follow the same format as session IDs (`^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$`) and must not start with `eu_`.

You can also manage end users directly:

| Method and path | Purpose |
|---|---|
| `POST /v1/end_users` | Create or update by `external_id` |
| `GET /v1/end_users` | List |
| `GET /v1/end_users/{eu}` | Read (`{eu}` is the `eu_` ID or your `external_id`) |
| `PATCH /v1/end_users/{eu}` | Update the name or traits |
| `DELETE /v1/end_users/{eu}` | **Erase** the end user and their data |

:::tip[Contact details belong in traits]
Phone numbers and emails should never be IDs. Put them in `traits`, and if you need a stable key derived from them, [hash it on your server](/docs/en/concepts/sessions/#keep-personal-data-out-of-ids).
:::

## Client tokens

A client token lets a browser or mobile app talk to the agent **as one verified end user**, without ever seeing your secret key. Your server mints it with a secret key (scope `runs:write`) and hands it to the client:

```bash
curl https://api.k-agent.kerneltics.com/v1/client_tokens \
  -H "Authorization: Bearer $KAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "store-assistant",
    "end_user": { "external_id": "cus_1042", "name": "Fahad" },
    "ttl_seconds": 900
  }'
```

```json
{
  "token": "ct_5RfQ0mXwJ8bT2nLk9pY4vC7hD1sG6aE3uZ0iWqNxb7K",
  "expires_at": 1791272820,
  "end_user": { "id": "eu_01k6rz3t5v7w9x1y2z4a6b8c0d", "object": "end_user", "external_id": "cus_1042", "name": "Fahad" }
}
```

- `ttl_seconds` is at most 3600; the default is 900 (15 minutes).
- Bind the token to one conversation with `session` (a `sess_` ID or `external_id`), or let the client open its own.
- `variables` may set only variables the agent declares as `client_settable`; secret variables are never stored in tokens.
- Tokens are stored hashed and can be revoked. A token stops working when it expires (`401 token_expired`), when it is revoked, when the key that minted it is revoked, or when its end user is erased.
- An open stream receives `event: error` with `{"code": "token_expired"}` when the token expires. Get a new token and reconnect with `Last-Event-ID`.

The [widget guide](/docs/en/guides/widget/#3-signed-in-users-verified) shows the full flow with Node and Python servers.

### What browser credentials may call

Publishable keys and client tokens are limited to a short list of routes. Anything else returns `403 principal_not_allowed`, and a session or run that belongs to another end user returns `404`.

| Credential | Allowed routes |
|---|---|
| Publishable key (`kt_pk_…`) | `GET /v1/widget/config?agent=…`, `POST /v1/widget/sessions` |
| Client token (`ct_…`) | `POST /v1/widget/sessions` · `GET /v1/sessions/{session}` · `GET` and `POST /v1/sessions/{session}/messages` · `GET /v1/sessions/{session}/events` · `POST /v1/sessions/{session}/handoff` · `POST /v1/sessions/{session}/close` · `GET /v1/runs/{run}` · `GET /v1/runs/{run}/events` · `POST /v1/runs/{run}/cancel` · `POST /v1/runs/{run}/submit_tool_outputs` |

With a client token, messages are sent as `role: "user"` only and cannot carry overrides, a version, another end user or session settings. Session metadata and variables are left out of responses, and listings never include internal notes or system messages. Run steps (the trace) are never available to browser credentials.

## Anonymous widget visitors

When the widget starts without a client token, `POST /v1/widget/sessions` creates an anonymous end user tied to a **device token** (`dt_…`). The device token is returned once, stored hashed, and lets the same browser resume its conversation later. Anonymous end users are never matched or merged with an `external_id`.

Anonymous traffic has its own limits: per IP, 20 widget sessions and 60 messages an hour; at most 40 messages per anonymous session; and, per agent, `widget.anonymous_daily_conversations` (default 200). Past a limit, visitors see a polite "try again later" notice.

## Erasure and retention

`DELETE /v1/end_users/{eu}` erases a person in one transaction, for data-subject requests under Saudi Arabia's PDPL:

- their sessions and runs, with all messages, steps, events, handoffs and tickets, are deleted;
- usage records stay for billing but lose the link to the person;
- the deletion is written to the audit log by ID only;
- their client tokens stop working at once.

It needs a secret key with `sessions:write`, or an admin in the dashboard. Separately, whole sessions are deleted automatically once they have been inactive longer than the project's `retention_days` (default 365).
