# End users and their keys

> Register the people your agents talk to, and mint each one a short-lived, prepaid ephemeral key that their device uses to hold a conversation.

An **end user** is one of *your* users: a person your agents talk to. Their conversations, what the agents remember about them and every relationship are filed under them. An **ephemeral key** is a short-lived token you mint for one end user on your server and hand to their device. It is the only credential that can hold a conversation.

The usual flow, on your server, at sign-in:

```bash title="curl"
# 1. The person, by your own id for them. Same id, same person, every time.
curl https://api.lumavox.ai/v1/end-users \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "user-4821" }'

# 2. A token for them, with a budget (5 credits) and a lifetime.
curl https://api.lumavox.ai/v1/ephemeral-keys \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "end_user_id": "user-4821", "credit_limit_micros": 5000000, "expires_in_seconds": 3600 }'
```

Send the token's `key` to the person's browser or app. Keep your API key on your server.

## End users

All `/v1/end-users` routes need an API key with the `agent` scope.

### Create one

`POST /v1/end-users` takes two optional fields:

| Field | What it is |
|---|---|
| `external_id` | **Your own id for this person**, up to 128 characters, unique in your organisation. Sending it makes the call idempotent: the same value always returns the same end user, created only the first time. |
| `display_name` | A name for your dashboard and for reading transcripts, up to 128 characters. Nothing resolves by it, and the agent never sees it unless you put it in the conversation yourself. |

```json title="Response — 201"
{
  "id": "eu_3kQ9x7MzT2pL8vNc",
  "external_id": "user-4821",
  "display_name": null,
  "origin": "created",
  "sessions": 0,
  "facts_updated_at": null,
  "created_at": "2026-10-24T09:20:11.000Z"
}
```

A new end user is answered `201`. A repeat of an existing `external_id` is answered `200` with the same person, so you can tell the two apart without comparing ids.

**Pass your `external_id` at every sign-in** and you never need to store the `eu_…` id. Everywhere the API takes an `end_user_id`, either form works.

Without an `external_id` you get a new person on every call, and **you must keep the returned `id`**: it is the only way back to what is remembered about them.

**One end user is one person.** What your agents remember is filed under the end user, so an id shared by several people — one `external_id` for every guest — shares what each of them said with all the others.

Your plan caps how many end users your organisation holds. Creating one past the cap is a 409; repeating an existing `external_id` is still answered as usual.

### Read, list and rename

- `GET /v1/end-users/{end_user_id}` reads one person: how many conversations they have had and when something was last learned about them (`facts_updated_at`, `null` while nothing has been).
- `GET /v1/end-users` lists them, newest first, a page at a time: `limit` (1 to 200, default 50) and `cursor`, answered as `data` and `next_cursor` ([Conventions](https://docs.lumavox.ai/conventions.md#lists-and-pages)). People once minted for a session opened without an end user (`origin: "generated"`) are left out unless you send `include_generated=true`.
- `PATCH /v1/end-users/{end_user_id}` changes `display_name` or `external_id`. Absent fields are untouched; `null` clears one. Another person already holding that `external_id` is a 409.

There is no way to change the `eu_…` id. Long-term memory is filed under it, so rewriting it would strand everything remembered about the person. Your `external_id` is the identifier you can change.

### Delete one

```bash title="curl"
curl -X DELETE https://api.lumavox.ai/v1/end-users/user-4821 \
  -H "Authorization: Bearer $API_KEY"
```

```json title="Response"
{ "id": "eu_3kQ9x7MzT2pL8vNc", "sessions_deleted": 4, "facts_deleted": 11 }
```

This removes the person, **every conversation they have had**, and everything remembered about them, and revokes their live keys. It cannot be undone. The id is never reissued. It answers what went with them, counted.

This is what to call when one of your users asks to be forgotten. To erase only the memory and keep the conversations, the person's own token can call `DELETE /v1/agent/facts` (see [Conversations and memory](https://docs.lumavox.ai/agents/conversations.md#forgetting)).

### What is remembered about them

Your server reads and writes what the agents remember about a person with your API key — no key minted for them:

| Call | Does |
|---|---|
| `GET /v1/end-users/{end_user_id}/facts` | Their facts, oldest first, a page at a time (`limit`, `cursor`, `include_replaced`). |
| `POST /v1/end-users/{end_user_id}/facts` | Tells the agents something: `{"text": "Is on the Gold plan.", "agent_id": null}`. `201` new, `200` when a fact already said it. |
| `PATCH /v1/end-users/{end_user_id}/facts/{fact_id}` | Corrects one fact's words; the old ones are kept as forgotten. |
| `DELETE /v1/end-users/{end_user_id}/facts/{fact_id}` | Forgets one, `204`. |

What you write is yours: a later conversation never replaces it. It works before the person has ever talked to an agent, so you can seed what your records hold at sign-up. A secret — a password, a code, a card, account or identity number, a key — is refused with `422`: facts are sent to the model on every turn. See [Conversations and memory](https://docs.lumavox.ai/agents/conversations.md#facts).

## Ephemeral keys

An ephemeral key speaks for **one end user**, may only talk to agents, and expires. With it, a device can open and read that person's conversations, talk on [the WebSocket](https://docs.lumavox.ai/agents/websocket.md) and read the key itself (`GET /v1/token`). It can reach nothing else: not another person, not speech or images, not your organisation's settings.

The `/v1/ephemeral-keys` routes need an API key with the `tokens` scope. The key you mint with must also hold `agent` (a master key holds everything). A token can never mint another token.

### Mint one

`POST /v1/ephemeral-keys`:

| Field | Required | What it is |
|---|---|---|
| `end_user_id` | yes | The person, by `eu_…` id or your `external_id`. Create them first. |
| `credit_limit_micros` | yes | The most this token may spend, as an integer of micro-credits: `5000000` is five credits ([Conventions](https://docs.lumavox.ai/conventions.md#credits)). There is no unlimited token. |
| `expires_in_seconds` | no | Its lifetime. 900 (15 minutes) when omitted. |
| `agent_ids` | no | The agents it may talk to. Omit it to allow every agent; an empty list is refused. |
| `scopes` | no | Must be `["agent"]`, the default. |
| `label` | no | Your own note, up to 120 characters. |

```json title="Response — 201"
{
  "id": "0193b1f0-2a7c-7b11-8d4e-91c0f5a2e6b3",
  "label": null,
  "prefix": "…_ek_live",
  "last4": "x9Qa",
  "scopes": ["agent"],
  "credit_limit_micros": 5000000,
  "credits_spent_micros": 0,
  "credits_remaining_micros": 5000000,
  "agent_ids": null,
  "created_by_api_key_id": "0193a8c2-51f0-7d2e-8b6a-4c1e9f3d2a70",
  "created_by_clerk_user_id": null,
  "end_user_id": "eu_3kQ9x7MzT2pL8vNc",
  "created_at": "2026-10-24T09:20:11.000Z",
  "expires_at": "2026-10-24T10:20:11.000Z",
  "last_used_at": null,
  "revoked_at": null,
  "revoked_reason": null,
  "settled_at": null,
  "active": true,
  "key": "<the token>"
}
```

**`key` is shown once.** It is signed, not stored, so it cannot be read back. Mint another if you lose it. `prefix` and `last4` are what you can recognise it by later, in a list.

### A key is prepaid

Its `credit_limit_micros` is set aside from your balance **when it is minted**. What it does not spend comes back when it expires, runs out or is revoked. If your balance cannot cover the limit, minting is refused with 402.

Every turn, transcription and memory step a conversation causes is charged to the token that caused it. When a token has spent its limit, the socket closes with `4004` and the reason "This token has spent its credit limit. Mint another to continue."

In your organisation's records — the list, and `GET /v1/ephemeral-keys/{key_id}` — `credits_spent_micros` is what the token had spent when it was last settled: when it ends, and daily for a long-lived one. So on a live token it can trail what the conversation has used. `settled_at` is set by the last settlement, and from then its spend and refund are final. The device reading its own token (`GET /v1/token`) sees its spend turn by turn.

### Limits

`GET /v1/ephemeral-keys/limits` returns the ceilings you can mint within — the platform's, and your plan's for one end user's token (`plan_*`, `null` where your plan sets none):

```json title="Response"
{
  "max_ttl_seconds": 2592000,
  "default_ttl_seconds": 900,
  "max_credit_limit_micros": 100000000000,
  "plan_max_ttl_seconds": 86400,
  "plan_max_credit_limit_micros": 50000000,
  "plan_max_live_keys": 5,
  "grantable_scopes": ["agent"]
}
```

A token is held to the tighter of each pair. Your plan also caps how many tokens you mint a minute and how much credit live tokens may hold at once. Nothing is clamped: a request past a cap is refused with 422 naming the rule, or 429 with `Retry-After` for the per-minute rate. [The server SDK](https://docs.lumavox.ai/sdks/server.md) clamps the lifetime for you: `ephemeralKeys.create` cuts `expires_in_seconds` to the tighter cap before it sends — pass `clampTtl: false` to be refused instead.

A token never outlives the API key that minted it. If that key expires sooner, so does the token.

### List and revoke

- `GET /v1/ephemeral-keys` lists your organisation's live tokens, a page at a time: `limit` (1 to 200, default 50) and `cursor`, answered as `data` and `next_cursor`. Send `live_only=false` to include expired and revoked ones.
- `GET /v1/ephemeral-keys/{key_id}` reads one, live or not.
- `DELETE /v1/ephemeral-keys/{key_id}` revokes one at once, and answers the revoked key — `active` `false`, `revoked_at` set. Open sockets on it close with `4004`. Anything already in flight still settles: the answer carries the figures from before, and `settled_at` is `null`. To read the final ones, read the token until `settled_at` is set — seconds, longer while a memory step it paid for still runs.

### What the device can read about its own key

A device holding a token can call `GET /v1/token` to learn its limit, what it has spent so far — turn by turn, not only as last settled — what is left and when it expires. Read it when a session starts and after a reply, not on a timer.

```bash title="curl"
curl https://api.lumavox.ai/v1/token -H "Authorization: Bearer $TOKEN"
```

```json title="Response"
{
  "kind": "ephemeral",
  "scopes": ["agent"],
  "credit_limit_micros": 5000000,
  "credits_spent_micros": 420000,
  "credits_remaining_micros": 4580000,
  "expires_at": "2026-10-24T10:20:11.000Z",
  "expires_in_seconds": 3412,
  "agent_ids": null,
  "end_user_id": "eu_3kQ9x7MzT2pL8vNc"
}
```

It tells the device nothing about your organisation or the key that minted it.

## Why an API key cannot talk to an agent

A key in a browser or an app can be read by anyone holding the device, and an API key can spend your whole balance and act as any of your users. A token can do neither: it speaks for one person, it expires, and it holds a fixed budget.

So `/v1/agent/*` refuses an API key with **403**:

```json title="Response — 403"
{
  "error": {
    "code": "forbidden",
    "message": "Conversations are reached with an end user's ephemeral key, not an API key. Mint one for the end user on your server (POST /v1/ephemeral-keys) and call this with it.",
    "request_id": "8f3c2a1b9d4e5f60",
    "issues": []
  }
}
```

The WebSocket closes an API key with `4002`.

## Next

- [Conversations and memory](https://docs.lumavox.ai/agents/conversations.md): open a session with the token
- [The WebSocket](https://docs.lumavox.ai/agents/websocket.md): talk on it
- [End users reference](https://docs.lumavox.ai/reference/end-users.md), [Ephemeral keys reference](https://docs.lumavox.ai/reference/ephemeral-keys.md)
