# End users

> The people your agents talk to and you generate on behalf of.

The people your agents talk to and you generate on behalf of.

When and why to call these: the [end users guide](https://docs.lumavox.ai/agents/end-users.md).

## List end users

`GET /v1/end-users`

Newest first.

**End users once minted for a session are hidden unless you ask for them** with `include_generated=true`: a conversation opened without an `end_user_id` used to create one. None is minted any more - a conversation needs a key, and a key its end user.

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

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `include_generated` | boolean |  | Also list the end users once minted for a session opened without one. Default `false`. |
| `limit` | integer |  | How many to answer, 1 to 200. 1–200. Default `50`. |
| `cursor` | string |  | The `next_cursor` of the page before. At least 1 characters. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — One page of end users, newest first |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of EndUserInfo |  |
| `data[].created_at` | string (date-time) |  |
| `data[].display_name` | string or null | The name you gave them, or null. |
| `data[].external_id` | string or null | Your own id for this person, if you gave one. Null otherwise — every `generated` end user has none. |
| `data[].facts_updated_at` | string (date-time) or null | When something was last learned or forgotten about this person. Null means nothing has ever been stored about them. |
| `data[].id` | string | The end user's identifier. Send it as `end_user_id` when opening a session, and keep it — it is how this person is recognised again, and what their long-term memory is filed under. |
| `data[].origin` | `"created"`, `"generated"` | `created` when you made this end user deliberately, `generated` when a session was opened without an id and one was minted for it. |
| `data[].sessions` | integer | How many conversations this person has had. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Create an end user

`POST /v1/end-users`

Somebody your agents will talk to.

**Send your own `external_id` and this is idempotent** — the same value always returns the same person, created only the first time. That is the recommended way to use it: pass your user id at every sign-in and you never have to store the `eu_...` we mint, while their memory and every relationship stay attached.

Send no `external_id` and you get a fresh person each call, and **must** keep the returned id — it is the only way back to what is remembered about them.

**Make the person before their key**: an ephemeral key is minted for one end user, and a conversation is opened with a key.

**Your plan caps how many end users you hold**: past it, a new one is a 409 naming the limit, while a repeat of an existing `external_id` is answered as ever.

**A repeat call answers `200` rather than `201`**, so the two cases are distinguishable without comparing ids.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/end-users" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string or null |  | A human name, for your dashboard and for reading a transcript. Nothing resolves by it — names are not unique — and it is never shown to the agent unless you put it in the conversation yourself. Optional, and worth leaving empty if you would rather not store a name here: `eu_...` is not personal data and a name is. At most 128 characters. |
| `external_id` | string or null |  | Your **own** id for this person — whatever you already call them. Optional, and unique within your organisation. **Sending it makes this call idempotent**: the same value always returns the same end user, creating one only the first time. That means you never have to store the id we mint — pass yours at every sign-in and the person is recognised, with their whole memory and every relationship intact. It can be used anywhere an `end_user_id` is accepted, interchangeably with the `eu_...` form. At most 128 characters. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The end user this `external_id` already named |
| `201` | `application/json` — The end user. The same `external_id` always returns the same one |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `409` | The organisation already holds as many end users as its plan allows. Delete one, or move to a larger plan. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `created_at` | string (date-time) |  |
| `display_name` | string or null | The name you gave them, or null. |
| `external_id` | string or null | Your own id for this person, if you gave one. Null otherwise — every `generated` end user has none. |
| `facts_updated_at` | string (date-time) or null | When something was last learned or forgotten about this person. Null means nothing has ever been stored about them. |
| `id` | string | The end user's identifier. Send it as `end_user_id` when opening a session, and keep it — it is how this person is recognised again, and what their long-term memory is filed under. |
| `origin` | `"created"`, `"generated"` | `created` when you made this end user deliberately, `generated` when a session was opened without an id and one was minted for it. |
| `sessions` | integer | How many conversations this person has had. |

## Read an end user

`GET /v1/end-users/{end_user_id}`

`end_user_id` may be the `eu_...` we minted **or** your own `external_id` — the two are interchangeable everywhere one is accepted.

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

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The end user |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `created_at` | string (date-time) |  |
| `display_name` | string or null | The name you gave them, or null. |
| `external_id` | string or null | Your own id for this person, if you gave one. Null otherwise — every `generated` end user has none. |
| `facts_updated_at` | string (date-time) or null | When something was last learned or forgotten about this person. Null means nothing has ever been stored about them. |
| `id` | string | The end user's identifier. Send it as `end_user_id` when opening a session, and keep it — it is how this person is recognised again, and what their long-term memory is filed under. |
| `origin` | `"created"`, `"generated"` | `created` when you made this end user deliberately, `generated` when a session was opened without an id and one was minted for it. |
| `sessions` | integer | How many conversations this person has had. |

## Rename an end user, or change your id for them

`PATCH /v1/end-users/{end_user_id}`

Absent fields are untouched; `null` clears one.

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

```bash title="curl"
curl -X PATCH "https://api.lumavox.ai/v1/end-users/$END_USER_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string or null |  | A human name, for your dashboard and for reading a transcript. Nothing resolves by it — names are not unique — and it is never shown to the agent unless you put it in the conversation yourself. Optional, and worth leaving empty if you would rather not store a name here: `eu_...` is not personal data and a name is. At most 128 characters. |
| `external_id` | string or null |  | Your **own** id for this person — whatever you already call them. Optional, and unique within your organisation. **Sending it makes this call idempotent**: the same value always returns the same end user, creating one only the first time. That means you never have to store the id we mint — pass yours at every sign-in and the person is recognised, with their whole memory and every relationship intact. It can be used anywhere an `end_user_id` is accepted, interchangeably with the `eu_...` form. At most 128 characters. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The updated end user |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id |
| `409` | Another end user in this organisation already has that `external_id`. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `created_at` | string (date-time) |  |
| `display_name` | string or null | The name you gave them, or null. |
| `external_id` | string or null | Your own id for this person, if you gave one. Null otherwise — every `generated` end user has none. |
| `facts_updated_at` | string (date-time) or null | When something was last learned or forgotten about this person. Null means nothing has ever been stored about them. |
| `id` | string | The end user's identifier. Send it as `end_user_id` when opening a session, and keep it — it is how this person is recognised again, and what their long-term memory is filed under. |
| `origin` | `"created"`, `"generated"` | `created` when you made this end user deliberately, `generated` when a session was opened without an id and one was minted for it. |
| `sessions` | integer | How many conversations this person has had. |

## Delete an end user and everything about them

`DELETE /v1/end-users/{end_user_id}`

Removes the end user, **every conversation they have had**, and everything remembered about them. Cannot be undone.

This is what to call when one of your users asks to be forgotten. `DELETE /v1/agent/facts` erases only the memory and leaves the conversations; this erases both.

The id is not reissued, and nothing that was filed under it is reachable again.

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

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The end user and every conversation with them are gone, counted |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | The end user that was deleted. |
| `facts_deleted` | integer | Durable facts erased with this person — all of them. |
| `sessions_deleted` | integer | Conversations this person had, all of them removed. |

## What is remembered about an end user

`GET /v1/end-users/{end_user_id}/facts`

The person's facts, **oldest first**, a page at a time — what `GET /v1/agent/facts` answers their own key, here for your server with no key to mint. `include_replaced=true` lists those a later conversation replaced or removed too, until they are deleted.

```bash title="curl"
curl "https://api.lumavox.ai/v1/end-users/$END_USER_ID/facts" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `include_replaced` | boolean |  | Also list the facts a later conversation replaced or removed, until they are deleted, the platform's `agents.memory.facts.replaced_kept_days` later (`GET /v1/platform/limits`). Default `false`. |
| `limit` | integer |  | How many to answer, 1 to 200. 1–200. Default `100`. |
| `cursor` | string |  | The `next_cursor` of the page before. At least 1 characters. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — One page of what is remembered, oldest first |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of FactInfo |  |
| `data[].agent_id` | string or null | Which agent was being talked to when this was learned. |
| `data[].confirmed_at` | string (date-time) or null | When a conversation last said it again; `null` for never. The agents see the most recently confirmed first when not every fact fits. |
| `data[].created_at` | string (date-time) |  |
| `data[].id` | string |  |
| `data[].replaced_at` | string (date-time) or null | When a later conversation replaced or removed it; `null` while it stands. Listed only with `include_replaced=true`. |
| `data[].replaced_by` | string or null | The fact that replaced it — moved city, changed job — or `null` when it was removed with nothing in its place, or stands. |
| `data[].scope` | `"person"`, `"agent"` | `person`: recalled by every agent of yours that shares its facts. `agent`: kept to one agent (`agent_id`), which alone recalls it — learnt by an agent whose `facts_scope` is `agent`, or added for that agent. |
| `data[].source` | `"conversation"`, `"customer"` | `conversation`: a reading learnt it. `customer`: you added or corrected it — a later conversation never replaces or removes it. |
| `data[].text` | string |  |
| `data[].updated_at` | string (date-time) | When it last changed: its `replaced_at` once replaced, else when you last corrected it, else its `created_at`. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Tell the agents something about an end user

`POST /v1/end-users/{end_user_id}/facts`

Adds a fact about the person, as you state it, for the agents to know from their next turn — what your records already hold: a name, a plan, a preference. It works before they have ever talked to an agent.

**It is yours** (`source` `customer`): a later conversation confirms it but never replaces or removes it. With `agent_id` only that agent recalls it. **Idempotent by its words**: a fact already saying it is answered `200`, now yours, and a new one `201`. Free.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/end-users/$END_USER_ID/facts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Prefers to be called Sam."}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes | What to remember about the person, as one plain statement: "Prefers to be called Sam." At most 500 characters. Never a secret — a password, a code, a card, account or identity number, or a key is refused, since facts are sent to the model on every turn. 1–500 characters. |
| `agent_id` | string or null |  | Keep it to one agent: only that agent recalls it. Unset or `null`: every agent of yours that shares its facts — and the ones that keep their own, too. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The fact that already said it, now yours |
| `201` | `application/json` — The fact, remembered |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id |
| `422` | No text, or text that looks like a secret — a password, a code, a card, account or identity number, or a key — which no fact may hold |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `agent_id` | string or null | Which agent was being talked to when this was learned. |
| `confirmed_at` | string (date-time) or null | When a conversation last said it again; `null` for never. The agents see the most recently confirmed first when not every fact fits. |
| `created_at` | string (date-time) |  |
| `id` | string |  |
| `replaced_at` | string (date-time) or null | When a later conversation replaced or removed it; `null` while it stands. Listed only with `include_replaced=true`. |
| `replaced_by` | string or null | The fact that replaced it — moved city, changed job — or `null` when it was removed with nothing in its place, or stands. |
| `scope` | `"person"`, `"agent"` | `person`: recalled by every agent of yours that shares its facts. `agent`: kept to one agent (`agent_id`), which alone recalls it — learnt by an agent whose `facts_scope` is `agent`, or added for that agent. |
| `source` | `"conversation"`, `"customer"` | `conversation`: a reading learnt it. `customer`: you added or corrected it — a later conversation never replaces or removes it. |
| `text` | string |  |
| `updated_at` | string (date-time) | When it last changed: its `replaced_at` once replaced, else when you last corrected it, else its `created_at`. |

## Correct what is remembered about an end user

`PATCH /v1/end-users/{end_user_id}/facts/{fact_id}`

Rewrites one fact. It becomes yours (`source` `customer`), so a later conversation never replaces it, and **the old words are kept as forgotten**: no reading of what was said before brings them back. A fact already replaced, or words another fact already says, are a 409. Free.

```bash title="curl"
curl -X PATCH "https://api.lumavox.ai/v1/end-users/$END_USER_ID/facts/$FACT_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Prefers to be called Sam."}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |
| `fact_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes | What to remember about the person, as one plain statement: "Prefers to be called Sam." At most 500 characters. Never a secret — a password, a code, a card, account or identity number, or a key is refused, since facts are sent to the model on every turn. 1–500 characters. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The fact, corrected |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id, or Unknown fact id |
| `409` | Replaced by a later conversation, or another fact already says that |
| `422` | No text, or text that looks like a secret — a password, a code, a card, account or identity number, or a key — which no fact may hold |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `agent_id` | string or null | Which agent was being talked to when this was learned. |
| `confirmed_at` | string (date-time) or null | When a conversation last said it again; `null` for never. The agents see the most recently confirmed first when not every fact fits. |
| `created_at` | string (date-time) |  |
| `id` | string |  |
| `replaced_at` | string (date-time) or null | When a later conversation replaced or removed it; `null` while it stands. Listed only with `include_replaced=true`. |
| `replaced_by` | string or null | The fact that replaced it — moved city, changed job — or `null` when it was removed with nothing in its place, or stands. |
| `scope` | `"person"`, `"agent"` | `person`: recalled by every agent of yours that shares its facts. `agent`: kept to one agent (`agent_id`), which alone recalls it — learnt by an agent whose `facts_scope` is `agent`, or added for that agent. |
| `source` | `"conversation"`, `"customer"` | `conversation`: a reading learnt it. `customer`: you added or corrected it — a later conversation never replaces or removes it. |
| `text` | string |  |
| `updated_at` | string (date-time) | When it last changed: its `replaced_at` once replaced, else when you last corrected it, else its `created_at`. |

## Forget one fact about an end user

`DELETE /v1/end-users/{end_user_id}/facts/{fact_id}`

Deletes one fact; no reading of what was said before brings it back. Their conversations stay — `DELETE /v1/end-users/{id}` removes the person and everything about them. Cannot be undone.

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

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `end_user_id` | string | yes |  |
| `fact_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `204` | The fact is gone |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown end user id, or Unknown fact id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
