---
title: "Knowledge"
description: "Upload documents, search them semantically, and ground your agents in your own content."
---

The knowledge store holds **documents** (uploaded files, pasted text, or
imported web pages) that agents can search mid-call via the
built-in `search_knowledge_base` tool. Attach documents (or grouping
**knowledge bases**) to an agent with the `knowledge_base_ids` /
`knowledge_document_ids` fields on [Agents](/api-reference/agents).

Documents are ingested asynchronously: uploads start in
`status="pending"`, move through `processing` (text extraction for
PDF/DOCX, chunking, embedding), and land on `ready` or `failed`.

<Note>
  All **write** operations on this page require the `admin` role (or
  an `sk_live_` key, which carries full access). Reads are available
  to every member.
</Note>

## Endpoints

The primary store is flat ("upload-first") under `/v1/knowledge/`:

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v1/knowledge/documents` | List documents |
| `POST` | `/v1/knowledge/documents` | Upload file(s) or create a text document |
| `GET` | `/v1/knowledge/documents/{document_id}` | Retrieve a document (includes extracted `content`) |
| `PATCH` | `/v1/knowledge/documents/{document_id}` | Rename / edit text content |
| `DELETE` | `/v1/knowledge/documents/{document_id}` | Delete a document |
| `POST` | `/v1/knowledge/documents/{document_id}/retry` | Re-ingest a `failed` document |
| `GET` | `/v1/knowledge/documents/{document_id}/original` | Download the original uploaded file |
| `POST` | `/v1/knowledge/documents/{document_id}/replace` | Replace the file behind a document |
| `POST` | `/v1/knowledge/documents/import-url` | Import a web page by URL |
| `POST` | `/v1/knowledge/documents/{document_id}/refresh` | Re-fetch a URL-imported page |
| `POST` | `/v1/knowledge/search` | Semantic search across documents |

Legacy **base-scoped** routes remain available as adapters (a base is
now just a named grouping over the flat store):

| Method | Path | Description |
|--------|------|-------------|
| `GET` / `POST` | `/v1/knowledge-bases` | List / create knowledge bases |
| `GET` / `PATCH` / `DELETE` | `/v1/knowledge-bases/{knowledge_base_id}` | Manage one base (delete detaches its documents, it does not delete them) |
| `GET` / `POST` | `/v1/knowledge-bases/{knowledge_base_id}/documents` | List / create documents inside a base |
| `PATCH` / `DELETE` | `/v1/knowledge-bases/{knowledge_base_id}/documents/{document_id}` | Manage a document inside a base |
| `POST` | `/v1/knowledge-bases/{knowledge_base_id}/documents/{document_id}/retry` | Re-ingest |
| `POST` | `/v1/knowledge-bases/{knowledge_base_id}/search` | Search within one base |

## Knowledge document object

```json
{
  "id": "8b2f4a1e-…",
  "name": "Refund policy.pdf",
  "source_type": "file",
  "original_filename": "Refund policy.pdf",
  "mime_type": "application/pdf",
  "byte_count": 482113,
  "estimated_chunk_count": 12,
  "status": "ready",
  "error": "",
  "chunk_count": 12,
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:31.040Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Document id |
| `name` | string | Display name |
| `source_type` | string | `text` (pasted/edited content) or `file` (uploaded) |
| `original_filename` | string | Empty for text documents |
| `mime_type` | string | Canonical MIME type (`text/plain`, `text/markdown`, `text/csv`, `application/pdf`, DOCX) |
| `byte_count` | integer | Size of the original upload (or the UTF-8 text) |
| `estimated_chunk_count` | integer | Persisted indexed count when ready; otherwise an estimate from current extracted content (falling back to the last persisted count until binary extraction is available) |
| `status` | string | `pending`, `processing`, `ready`, or `failed` |
| `error` | string | Ingestion error summary when `failed` |
| `chunk_count` | integer | Chunks actually indexed |
| `source_origin` | string | `upload`, `url`, or `null` |
| `source_url` | string | The imported page address for `url`-origin documents, else `null` |
| `fetched_at` | timestamp | Last successful fetch of `source_url` (`null` while a re-fetch is queued) |
| `created_at`, `updated_at` | timestamp | |

The **detail** endpoint (`GET /v1/knowledge/documents/{id}`) adds:
`content` (the canonical extracted text), `uploaded_by`
(`{id, name, email}` or `null`), `replaced_at`, `has_original`,
`original_size_bytes`, and `original_mime_type`.

### Upload limits

| Kind | Extensions | Max size |
|------|-----------|----------|
| Text | `.txt`, `.md`, `.csv` | 1 MB |
| Binary | `.pdf`, `.docx` | 50 MB |

Anything else is rejected with a `400` field error naming the file.

---

## List documents

<CodeGroup>
```bash cURL
curl 'https://api.thunderphone.com/v1/knowledge/documents?status=ready' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

| Query param | Description |
|-------------|-------------|
| `q` | Substring match on name or content |
| `status` | `pending`, `processing`, `ready`, `failed` |

Returns a plain array of [document objects](#knowledge-document-object),
newest first.

---

## Create documents

Two content types are accepted:

**Multipart upload** — one or more files in `files[]` (or a single
`file` part, optionally with a `name` field):

<CodeGroup>
```bash cURL (files)
curl -X POST https://api.thunderphone.com/v1/knowledge/documents \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -F 'files[]=@refund-policy.pdf' \
  -F 'files[]=@faq.md'
```

```bash cURL (text)
curl -X POST https://api.thunderphone.com/v1/knowledge/documents \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Escalation playbook", "content": "When a caller asks for a manager…"}'
```
</CodeGroup>

**JSON** — `{name, content}` creates a `source_type="text"` document.

```json Response (201 Created)
{
  "documents": [ /* document objects, status "pending" */ ],
  "warnings": [
    {
      "document_id": "8b2f4a1e-…",
      "duplicate_document_ids": ["77aa…"],
      "message": "This file has the same content as an existing document."
    }
  ]
}
```

Every file is validated before anything is created — one bad file
fails the whole request with `400`. Duplicate-content uploads are
allowed but reported in `warnings`.

---

## Retrieve, edit, delete

`GET /v1/knowledge/documents/{id}` returns the
[detail shape](#knowledge-document-object) including extracted
`content`.

`PATCH` accepts JSON `{name?, content?}` (or a multipart `file` to
swap content). Changing `name` or `content` re-queues ingestion
(`status` returns to `pending`). Returns the updated document.

`DELETE` returns `204 No Content` and removes the document and its
chunks from the index.

---

## Download the original file

For `file`-sourced documents, fetch the exact bytes that were uploaded.

<CodeGroup>
```bash cURL
curl 'https://api.thunderphone.com/v1/knowledge/documents/8b2f4a1e-…/original?download=1' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

Returns either the file stream directly or a short-lived signed URL:

```json
{ "url": "https://storage.googleapis.com/…", "expires_in": 900 }
```

`download=1` sets an attachment `Content-Disposition`. Returns `404`
when no original is stored (text documents).

## Replace the file

`POST /v1/knowledge/documents/{id}/replace` with a multipart `file`
part swaps the document's content for the new file (same validation
and limits as upload), stores the new original, stamps `replaced_at`,
and re-queues ingestion.

```json Response
{ "document": { /* document object, status "pending" */ }, "warnings": [] }
```

If the uploaded bytes are identical to the current file, the request
still succeeds and a warning says so.

## Retry ingestion

`POST /v1/knowledge/documents/{id}/retry` re-queues a **failed**
document. Returns the document with `status="pending"`, or `400` with
`{"detail": "Only failed documents can be retried."}` if it is not in
`failed`.

---

## Import a web page by URL

Creates a document from a public web page. The page is fetched
**server-side and asynchronously**: the document is returned
immediately in `status="pending"`, then the ingestion worker fetches
the URL (through the same SSRF-guarded transport as webhooks — private
networks, localhost, and cloud metadata addresses are refused, with at
most 5 redirects, each hop re-validated), extracts the main content
(HTML boilerplate such as navigation and footers is stripped;
`application/pdf` responses go through PDF text extraction), and
indexes it. Pages are capped at 5 MB.

If no `name` is given, the document is named after the URL and adopts
the page title once fetched.

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/knowledge/documents/import-url \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/pricing" }'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | yes | `http://` or `https://` page address (bare domains default to `https://`) |
| `name` | string | no | Display name; defaults to the URL, then the page title |

```json Response (201)
{
  "document": { /* document object, status "pending", source_origin "url" */ },
  "warnings": []
}
```

`warnings` lists earlier documents that were imported from the same
URL. Pages that can't be fetched (HTTP errors, blocked destinations)
or contain no extractable text (for example JavaScript-only apps) land
in `status="failed"` with a readable `error`.

### Refresh a URL-imported document

`POST /v1/knowledge/documents/{document_id}/refresh` re-fetches
`source_url` and re-indexes the document (admin role required; only
`url`-origin documents can be refreshed). The stored `ETag` /
`Last-Modified` validators are sent, so an unchanged page is a cheap
no-op that keeps the existing index.

---

## Search

Semantic (embedding) search over ready documents. This is the same
retrieval the agent's `search_knowledge_base` tool uses at call time.

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/knowledge/search \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "what is the refund window?"}'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | yes | ≤ 4000 chars |
| `document_ids` | array of UUID | no | Restrict the search scope; omit to search everything |

```json Response
{
  "results": [
    {
      "text": "Refunds are available within 30 days of purchase…",
      "document": "Refund policy.pdf",
      "score": 0.874312
    }
  ]
}
```

Results are ranked by cosine similarity (`score` closer to 1 is
better), capped per document so one file can't fill every slot, and
each `text` is length-capped.

`POST /v1/knowledge-bases/{id}/search` takes the same body and scopes
the search to that base's documents.

---

## Knowledge bases (grouping)

A knowledge base is a named group:
`{id (UUID), name, description, document_count, created_at, updated_at}`.
Create with `POST /v1/knowledge-bases` `{name, description?}` (`201`);
update with `PATCH`; `DELETE` (`204`) detaches the base's documents
back into the flat store without deleting them. Attach whole bases to
an agent via `knowledge_base_ids`.

---

## Related

<CardGroup cols={2}>
  <Card title="Agents" icon="robot" href="/api-reference/agents">
    Attach documents and bases via `knowledge_document_ids` / `knowledge_base_ids`.
  </Card>
  <Card title="Calls" icon="phone" href="/api-reference/calls">
    Knowledge lookups appear in the call history as tool calls.
  </Card>
  <Card title="Reusable instructions" icon="list-check" href="/api-reference/reusable-instructions">
    For rules the agent must always follow, put them in the prompt instead of a lookup.
  </Card>
</CardGroup>
