# Knowledge

> Give your agents reference material, written, from a web page or from a file, that they look up by meaning, and choose which agent reads which entry.

**Knowledge** is reference material your agents can draw on: opening hours, a returns policy, a product catalogue, a help centre. You write entries once for your organisation and assign each to any number of agents. When a question comes up, the agent looks the answer up by meaning rather than relying on what the model happens to know.

```bash title="curl"
curl https://api.lumavox.ai/v1/knowledge \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Opening hours",
    "category": "Visiting",
    "body": "We are open 9:00 to 18:00, Tuesday to Sunday. Closed on Mondays and on 25 December.",
    "agent_ids": ["0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07"]
  }'
```

```json title="Response — 201"
{
  "id": "0193b210-4b9e-7e21-a3c7-58d0f1b2c946",
  "title": "Opening hours",
  "category": "Visiting",
  "body": "We are open 9:00 to 18:00, Tuesday to Sunday. Closed on Mondays and on 25 December.",
  "summary": null,
  "status": "pending",
  "body_chars": 83,
  "source_kind": "text",
  "source_name": null,
  "refresh": null,
  "refreshed_at": null,
  "searchable": false,
  "summary_source": null,
  "error": null,
  "chunks": 0,
  "agents": [{ "id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07", "name": "Mira" }],
  "created_at": "2026-10-24T09:55:02.000Z",
  "updated_at": "2026-10-24T09:55:02.000Z",
  "version": 1
}
```

The entry comes back `pending` and **no agent can find it yet**. Indexing takes seconds to a few minutes: an entry is `ready` once the search index returns it. Poll `GET /v1/knowledge/{entry_id}` until `status` is `ready`, or `failed` with the reason in `error`.

## Scopes

Reading, listing and testing knowledge need `agent`. Writing, assigning and deleting need `agent:write`. Writing or editing an entry, importing a page or a file, re-indexing and refreshing also count against your rate limit like a generation, because indexing is model work.

## Write an entry

`POST /v1/knowledge`:

| Field | What it is |
|---|---|
| `title` | Required, 1 to 255 characters. |
| `body` | Required, up to 100,000 characters. |
| `category` | Optional, up to 120 characters. For your own filtering. |
| `summary` | Usually left out. See below. |
| `agent_ids` | The agents that may use it. May be empty: an entry nobody has yet is a draft, not a fault. Ids that are not yours are skipped. |

**The summary is what the agent sees on every turn.** The body is only fetched when the agent decides to look something up, so a vague summary makes it less likely to consult a good entry. The indexer writes a one-line summary for you (`summary_source: "model"`) and rewrites it with every new body. If you write your own (up to 200 characters, `summary_source: "customer"`), it is kept through every later edit. Sending `summary: null` on an edit hands it back to the indexer.

### Status

| `status` | Meaning |
|---|---|
| `importing` | A page or file is being read into text. |
| `pending` | The latest body is being indexed. |
| `ready` | Indexed. |
| `failed` | Indexing failed; the reason is in `error`. |

`status` is about the **latest** body. `searchable` says whether an agent can find the entry at all. An edited entry stays searchable at its previous version while the new one indexes, and keeps that version if the new one fails.

## From a web page

```bash title="curl"
curl https://api.lumavox.ai/v1/knowledge/urls \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/help/returns", "refresh": "weekly", "agent_ids": ["0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07"] }'
```

The page is fetched, read into text and indexed. The entry comes back `202`, `importing`. A PDF, Word, CSV or text file at a public address works the same way. Only the public internet is reached.

| Field | What it does |
|---|---|
| `url` | Required. A public `http` or `https` address. |
| `refresh` | `off` (the default), `daily` or `weekly`: read the page again on that schedule and re-index it when it has changed. |
| `crawl` | Also import the pages this one links to, and those the site's sitemap lists, in the same part of the site (`/help/…` finds `/help/…` and nothing else). One level deep, `robots.txt` honoured, pages already imported skipped. Each becomes its own entry with the same agents, category and `refresh`. |
| `max_pages` | With `crawl`: the most entries made, this page included. 2 to 100, default 25. |
| `title`, `category`, `agent_ids` | As for a written entry. |

`POST /v1/knowledge/{entry_id}/refresh` reads a page-made entry again now, rather than at its next scheduled refresh.

## From a file

Files are uploaded straight to storage. **The bytes never pass through the API.**

```bash title="curl"
# 1. Ask where to upload it.
curl https://api.lumavox.ai/v1/knowledge/upload-url \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "catalogue.pdf", "size_bytes": 482113 }'
# → { "upload_id": "…", "url": "https://…", "content_type": "application/pdf", "expires_at": "2026-10-24T10:05:00.000Z" }

# 2. PUT the bytes there, with exactly that Content-Type and no Authorization header.
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @catalogue.pdf

# 3. Make the entry.
curl https://api.lumavox.ai/v1/knowledge/files \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "upload_id": "'"$UPLOAD_ID"'", "filename": "catalogue.pdf", "agent_ids": ["0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07"] }'
```

- **Formats**, by the file's extension: `.pdf`, `.docx`, `.xlsx`, `.xls`, `.ods`, `.odt`, `.csv`, `.html`, `.htm`, `.txt`, `.md`.
- **Up to 10 MB.** Larger is a 413.
- **A CSV with `question` and `answer` columns** becomes one question-and-answer pair per row.
- The upload URL stops accepting the file at `expires_at`. Without a `content_type` in the request, the ticket names the one the file's extension calls for: send exactly that.

The entry comes back `202`, `importing` while the file is read, then `pending` while it is indexed, then `ready`. The extracted text becomes the entry's body, editable like any other. Without a `title` the file's own is used.

## Assign entries to agents

**Entries belong to your organisation, not to an agent.** Assigning only says which agents may read which entries. There are three ways to do it, and all of them are declarative or idempotent:

| Call | What it sets |
|---|---|
| `PUT /v1/knowledge/{entry_id}/agents` | The complete set of agents that have **this entry**: `{"agent_ids": [...]}`. |
| `PUT /v1/agents/{agent_id}/knowledge` | The complete set of entries **this agent** has: `{"entry_ids": [...]}`. |
| `POST /v1/knowledge/bulk` | `assign` or `unassign` agents on up to 200 entries at once, leaving each entry's other agents alone; or `delete` up to 200 entries. It answers how many `entries` it acted on, how many links or entries it `changed`, and the ids it `skipped`. |

```bash title="curl"
curl -X PUT https://api.lumavox.ai/v1/agents/$AGENT_ID/knowledge \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "entry_ids": ["0193b210-4b9e-7e21-a3c7-58d0f1b2c946"] }'
```

```json title="Response"
{ "added": 1, "removed": 0, "skipped": [] }
```

An empty list clears every assignment, which is a real thing to want and not an error. Nothing is created or deleted by assigning. Ids that are not yours come back in `skipped` rather than failing the call.

`GET /v1/agents/{agent_id}/knowledge` lists the entries an agent has, newest first, in one page (`next_cursor` is `null`). Only `ready` entries are visible to the agent itself.

An agent's `knowledge_enabled` switch (on by default) turns its knowledge off without unassigning anything, which is how to try an agent without its reference material.

## How an agent reads its knowledge

Three fields on the [agent](https://docs.lumavox.ai/agents.md) decide it:

| Field | What it does |
|---|---|
| `knowledge_mode` | `auto` (the default) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens`, so there is nothing to look up and nothing to miss, and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` ([Limits](https://docs.lumavox.ai/limits.md)); the more there is, the slower each turn's first word. `lookup` always looks it up. |
| `knowledge_lookup_first` | **On by default**: search before every reply and hand the model what matches, so it never answers from memory what its material says. Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `knowledge_max_results` | How many pieces a lookup hands the model, 1 to 8. 4 when unset. |

A lookup hands over only pieces with a similarity score of at least the platform's `agents.knowledge.min_score` ([Limits](https://docs.lumavox.ai/limits.md)). What the agent looked up for each reply is reported live on the socket as a `knowledge` event, and stored on the message as `knowledge` (see [Conversations and memory](https://docs.lumavox.ai/agents/conversations.md#read-a-conversation)).

## Test a question

`POST /v1/knowledge/search` runs one question exactly as an agent's lookup would, and returns the pieces it would be handed with their scores. It costs nothing and changes nothing.

```bash title="curl"
curl https://api.lumavox.ai/v1/knowledge/search \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "Are you open on Mondays?", "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07" }'
```

```json title="Response"
{
  "outcome": "found",
  "mode": "lookup",
  "hits": [
    {
      "entry_id": "0193b210-4b9e-7e21-a3c7-58d0f1b2c946",
      "title": "Opening hours",
      "text": "We are open 9:00 to 18:00, Tuesday to Sunday. Closed on Mondays and on 25 December.",
      "score": 0.82
    }
  ],
  "embed_ms": 41,
  "query_ms": 23
}
```

- `query` is a question as a person would ask it, up to 500 characters.
- Name `agent_id` to search what that agent has; omit it to search every entry. An agent that is not yours is a 404.
- `outcome` is `found`, `none` (nothing relevant enough) or `failed`.
- With `agent_id`, `mode` says whether that agent looks things up (`lookup`) or has its whole knowledge in its prompt (`prompt`). The hits show what a lookup would find either way.

## Find and understand entries

- `GET /v1/knowledge` lists every entry, most recently changed first, with its agents. `q` is a plain text match over title, summary and body, **not** the semantic search the agent uses. `agent_id`, `category` and `status` narrow further and combine. It is a page: `limit` (1 to 200, default 50) and `cursor`, answered as `data` and `next_cursor` ([Conventions](https://docs.lumavox.ai/conventions.md#lists-and-pages)).
- `GET /v1/knowledge/{entry_id}` reads one entry, with how many chunks it was indexed into and which agents have it.
- `GET /v1/knowledge/categories` lists every category in use, alphabetically, as strings in one page: `{"data": ["Returns", "Visiting"], "next_cursor": null}`.
- `GET /v1/knowledge/insights` shows which entries your agents drew on, by how many replies, and the newest questions whose lookups found nothing: the gaps worth filling. `days` sets the window, 1 to 90, default 7. An agent with its knowledge in its prompt looks nothing up, so it does not appear here.

## Edit, re-index and delete

`PATCH /v1/knowledge/{entry_id}` is a partial update of `title`, `body`, `category`, `summary`, `refresh` (for a page) and `agent_ids` (the complete set, saved in the same step). An explicit `null` clears `category`, and on `summary` hands the line back to the indexer.

- **Only a changed `body` costs anything.** It re-indexes: `status` returns to `pending`, and **the previous version stays searchable until the new one is ready**, so no agent loses the entry meanwhile. Renaming or recategorising is immediate.
- Send `expected_version` (the `version` you read) to be refused with 409 if somebody edited the entry since.
- A written entry's body stays within 100,000 characters.

`POST /v1/knowledge/{entry_id}/reindex` queues an entry for indexing again as it stands, for example one that `failed` while a provider was down. Entries stuck `pending` are retried on their own after half an hour.

`DELETE /v1/knowledge/{entry_id}` removes the entry, everything indexed from it and every assignment to it, and answers **204** with no body. **This affects every agent that had it.** To take it from one agent only, change that agent's set with `PUT /v1/agents/{agent_id}/knowledge`.

## When changes reach conversations

An edit, an assignment or a deletion that an agent can see reaches its open conversations within about a minute, from their next turn, the same way an edit to the agent does. A new body is used once it is `ready`; until then the previous version answers.

## Limits

| Limit | Value |
|---|---|
| A written entry's body | 100,000 characters |
| Text read from a page or file | 1,000,000 characters |
| An uploaded file | 10 MB |
| One bulk call | 200 entries, 100 agents |
| A test query | 500 characters, up to 8 results |
| Entries per organisation | capped by the platform; past the cap, a new entry is a 409 |

## Next

- [Agents](https://docs.lumavox.ai/agents.md): the switches that decide how knowledge is read
- [The WebSocket](https://docs.lumavox.ai/agents/websocket.md): the `knowledge` event
- [Knowledge reference](https://docs.lumavox.ai/reference/knowledge.md)
