Knowledge
Upload documents, get hybrid semantic and keyword search with citations, and give your agents grounded answers over your own content.
Snippet setup
The Python and TypeScript snippets on this page assume BASE_URL = "https://api.2kw.ai" and your API key. Python: import requests, api_key = "sk_your_api_key" and headers = {"Authorization": f"Bearer {api_key}"}. TypeScript: const BASE_URL = "https://api.2kw.ai" and const apiKey = "sk_your_api_key".
How It Works
A knowledge base is a searchable index over documents you upload. 2kw.ai parses each document, splits it into chunks, embeds the chunks with the embedding model you chose, and stores both the vectors and a keyword index. A search runs dense and lexical retrieval side by side and fuses the two rankings.
The workflow is: create a knowledge base → upload documents → search, or attach it to an agent → resolve citations.
Everything on this page is also available in the console under Knowledge in the sidebar: a list, a create form, and a detail page with Documents, Playground and Settings tabs.
Two ways to use it
Call POST /v1/knowledge-bases/{id}/search yourself and build on the ranked passages, or attach the knowledge base to an agent with the built-in file_search tool and let the model search and cite. Both paths use the same retrieval.
Concepts
| Term | Meaning |
|---|---|
| Knowledge base | The index: an embedding provider and model, a chunking configuration, and the documents in it. Configuration changes are versioned. |
| Document | One logical source, identified by its filename. Uploading a file with the same name again creates a new revision of the same document. |
| Revision | One ingested version of a document, with its own status. Retrieval serves the newest READY revision. |
| Chunk | A passage of a revision, with page range and heading path where the source has them. Search results and citations point at chunks. |
| Citation | A pointer from an agent's answer to the exact chunk it used. Citations stay resolvable after re-ingests and deletions. |
Creating a Knowledge Base
POST /v1/knowledge-bases
Request
curl -X POST https://api.2kw.ai/v1/knowledge-bases \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_your_api_key" \
-d '{
"name": "Product manuals",
"slug": "product-manuals",
"description": "Installation and service manuals, current models",
"embeddingProviderId": "your-provider-id",
"embeddingModel": "text-embedding-3-small",
"embeddingDim": 1536,
"chunkingStrategy": "HIERARCHICAL",
"hybridSearchEnabled": true
}'
Returns 201 Created with the knowledge base.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
slug | string | Yes | Lowercase letters, digits and hyphens. Unique within the organization; a collision returns 409 |
description | string | No | Free text, up to 4000 characters |
embeddingProviderId | string | Yes | The id of one of your configured AI providers, or builtin for the platform's built-in embedding models |
embeddingModel | string | Yes | An embedding model that provider serves. See the catalog below |
embeddingDim | integer | Yes | Vector width. 1536 is the provisioned width; a model that cannot serve it is refused with 400 |
chunkingStrategy | string | No | HIERARCHICAL (default), FIXED, AUTO or CUSTOM |
chunkSize | integer | No | Tokens per chunk, 50 to 4000. Default 400 |
chunkOverlap | integer | No | Overlap between neighbouring chunks, 0 to 2000. Default 50 |
parentChunkSize | integer | No | Hierarchical only. Size of the parent passage returned around a matching child, 100 to 8000. Default 1500 |
hybridSearchEnabled | boolean | No | Run keyword retrieval next to vector retrieval. Default true |
rerankerProviderId | string | No | Reserved for a reranking provider. Leave empty. builtin is refused with 400: there is no built-in reranker |
textSearchLanguage | string | No | Text-search language for the keyword leg — stemming and stop words. Default simple. See Text-search language below |
The embedding dimension is fixed at creation
embeddingDim cannot be changed later, and neither can the embedding model without re-ingesting every document, which the API does not do for you yet. Pick the model first, then upload.
Which embedding models fit
GET /v1/providers/{providerId}/embedding-models
Lists the embedding models a provider serves at a provisioned width. The console's create form uses this to fill the model dropdown. Pass builtin as providerId to list the built-in embedding models; the list is empty when the platform offers none.
{
"models": [
{ "id": "text-embedding-3-small", "dimensions": 1536 },
{ "id": "text-embedding-3-large", "dimensions": 1536 }
],
"supportedDimensions": [1536]
}
A model appears once per width it can serve, so a model with a larger native width is offered at 1536. Providers without a catalogued embedding model return an empty list, never an error; you can still pass a model id by hand. Creating the knowledge base tries that model on the provider first and answers 400 when the provider does not serve it at the requested width, so nothing is saved that cannot embed.
Chunking strategies
| Strategy | What it does | Use it when |
|---|---|---|
HIERARCHICAL | Indexes small child chunks for precise matching and returns the surrounding parent passage (parentChunkSize) as the result. | Default. Manuals, contracts, reports: anything where a match needs its context. |
FIXED | Fixed-window chunks of chunkSize tokens with chunkOverlap. The chunk itself is the result. | Short, uniform records: tickets, FAQ entries, product rows. |
AUTO | Today identical to HIERARCHICAL. Reserved for a per-document choice later. | Only if you want the platform's future default without changing configuration. |
CUSTOM | Not supported yet. A knowledge base with this strategy fails ingestion. | Do not use. |
Chunking settings apply to documents ingested after the change. Existing revisions keep the chunks they were indexed with.
Text-search language
The keyword half of hybrid search matches with a Postgres text-search configuration — stemming and a stop-word list, so a search for "invoices" also matches "invoice", and common words like "the" or "die" do not drown out the terms that actually distinguish a passage. textSearchLanguage picks that configuration; the default, simple, applies neither: no stemming, no stop words, which is the safe choice for a mixed-language corpus but the weakest match for a single-language one.
Stemming joins inflected forms of the same word ("invoices" → "invoice", plural and case endings collapsing onto a shared root). It does not join a word with an unrelated derived form — a German knowledge base set to german still treats "klassifiziert" and "Klassifizierung" as different words, since they are not the same inflection of one root. Matching across word families like that is the job of the vector half of hybrid search, not the keyword one.
Any Postgres text-search configuration works: english, german, french, spanish, and every other configuration Postgres 17 ships. Pick the language most of the knowledge base's content is written in.
Changing textSearchLanguage does not rewrite anything inline. A background process rebuilds the knowledge base's keyword index in the new language, and search keeps answering from the current language and its existing rows until that finishes — never a mix of both. In the console, the knowledge base's Settings tab shows "Reindexing to <language>…" under the field while this is in progress; the console achieves this by leaving textSearchLanguage out of the request entirely on a save that did not touch it — never by sending the value back.
Do the same from the API: omit textSearchLanguage from a PATCH to leave an in-progress reindex untouched. Sending it back explicitly is not equivalent, even with the value GET just returned — GET reports the active language as textSearchLanguage, and sending that value back while a different one is pendingTextSearchLanguage is the request's cancellation case: it abandons the pending reindex and discards the rows it already built.
Built-in embedding models
A knowledge base does not need a provider of your own. Set embeddingProviderId to builtin and it embeds through the platform's own models:
{
"name": "Product manuals",
"slug": "product-manuals",
"embeddingProviderId": "builtin",
"embeddingModel": "text-embedding-3-large",
"embeddingDim": 1536
}
| Model | Stored width | Price per million input tokens |
|---|---|---|
text-embedding-3-small | 1536 | €0.0825 |
text-embedding-3-large | 1536 | €0.53625 |
Both models store 1536-wide vectors: text-embedding-3-small emits that width natively, and text-embedding-3-large is asked for 1536 dimensions on every call. GET /v1/providers/builtin/embedding-models lists the models the platform offers right now. Prices are net of VAT; see the rate card.
Documents and search queries are embedded on the platform's Azure OpenAI deployments in the EU Data Zone, the same processor and region as the built-in chat models. Creating the knowledge base checks the model against the platform's catalogue without calling it, so the check costs nothing.
The same models are callable on their own through the gateway's OpenAI-compatible POST /v1/embeddings, charged at the same rate.
Every embedding call, at ingestion and for each search query, counts toward your plan's usage and is charged for the tokens it used. When a call is refused, for example because your plan's usage is used up and no extra usage is available:
- Ingestion: the revision ends in
ERRORwith the refusal in itserror, and the revision retrieval already serves stays as it was. Nothing resumes by itself; once usage is available again, retry the document withPOST /v1/knowledge-bases/{knowledgeBaseId}/documents/{documentId}/retry, which embeds it again from the start. - Search: the request answers with the refusal's status,
402when usage is used up, instead of falling back to keyword results withlexical_only.
Switching an existing knowledge base
A PATCH can move embeddingProviderId from one of your providers to builtin or back. The model check runs on the new provider, but nothing is embedded again: the stored vectors stay comparable only if the new provider serves the same model at the same width, and a model name does not prove that (on Azure OpenAI it is a deployment name you chose). The safe way to move is a new knowledge base on the new provider, with the documents uploaded again. The console offers no switch.
Uploading Documents
POST /v1/knowledge-bases/{knowledgeBaseId}/documents
Multipart upload. The form field is files and takes one or more files.
Request
curl -X POST https://api.2kw.ai/v1/knowledge-bases/kb_123/documents \
-H "Authorization: Bearer sk_your_api_key" \
-F "files=@service-manual-2026.pdf" \
-F "files=@installation-guide.docx"
The response is always 202 Accepted, with one entry per file:
[
{
"filename": "service-manual-2026.pdf",
"upload": {
"document": { "id": "doc_…", "name": "service-manual-2026.pdf", "knowledgeBaseId": "kb_123" },
"acceptedVersion": { "id": "ver_…", "versionNo": 1, "status": "PENDING", "mime": "application/pdf", "byteSize": 1834022 }
}
},
{
"filename": "notes.xyz",
"error": "Unsupported file type"
}
]
Files are accepted independently. One rejected file does not discard the others.
| Rule | Value |
|---|---|
| Formats | The same 17 input formats as Document Conversion: PDF, DOCX, XLSX, PPTX, CSV, MSG, EML, HTML, Markdown, TXT, AsciiDoc, ZIP, images, DXF, DWG, STEP, GEO |
| Size | 25 MB per file, 104 MB per request. A file over the per-file cap is rejected with 413 file_too_large; split larger batches into several requests |
| Same filename | Creates a new revision of the existing document. The previous revision keeps serving retrieval until the new one is READY, then the switch is atomic |
| Storage | Document bytes are kept for as long as the document exists, so a revision can be re-served and its citations resolved |
Ingestion status
Ingestion is asynchronous. Poll the accepted revision until it reaches a terminal state:
GET /v1/knowledge-bases/{knowledgeBaseId}/documents/{documentId}/versions/{versionId}
| Status | Meaning |
|---|---|
PENDING | Accepted, waiting for a worker |
PARSING | Converting the file to text and structure |
CHUNKING | Splitting into chunks |
EMBEDDING | Calling the embedding model |
READY | Searchable. Terminal |
ERROR | Failed. Terminal; error on the revision says why |
GET /v1/knowledge-bases/{knowledgeBaseId}/documents lists documents with the revision retrieval is serving, newest first. Filter with ?status=READY (or any other status); the filter applies to the current revision.
Attaching a file you already uploaded
A file uploaded through POST /v1/files with purpose=knowledge can be ingested without a second upload: POST /v1/knowledge-bases/{knowledgeBaseId}/documents/attach with {"fileId": "file_…"}. It answers 202 with the accepted revision, 400 if the file's purpose is not knowledge, 404 for an unknown file and 410 for a deleted one.
Searching
POST /v1/knowledge-bases/{knowledgeBaseId}/search
Request
curl -X POST https://api.2kw.ai/v1/knowledge-bases/kb_123/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_your_api_key" \
-d '{
"query": "torque for the M8 mounting bolts",
"topK": 5
}'
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Natural-language question or keywords |
topK | integer | No | Number of hits. Default 8, clamped to 1 to 100 |
metadataFilter | object | No | Key/value filter on document metadata |
Response
{
"query": "torque for the M8 mounting bolts",
"hits": [
{
"hitId": "h_0",
"knowledgeBaseId": "kb_123",
"documentId": "doc_…",
"documentVersionId": "ver_…",
"documentName": "service-manual-2026.pdf",
"chunkId": "chk_…",
"content": "Mounting. Tighten the four M8 bolts to 25 Nm in a cross pattern …",
"matchedSpan": "M8 bolts to 25 Nm",
"pageStart": 14,
"pageEnd": 14,
"headingPath": ["Installation", "Mounting"],
"score": 0.0325
}
]
}
| Field | Description |
|---|---|
content | The passage. Under hierarchical chunking this is the parent passage around the matching child |
matchedSpan | The part of content that matched most closely, when available |
pageStart, pageEnd | Page range in the source, absent for sources without pages |
headingPath | The heading hierarchy above the passage |
score | Reciprocal Rank Fusion score. Useful for ordering, not comparable across queries |
chunkId, documentVersionId | Stable ids you can store and later resolve as a citation |
Search is top-k and not paginated. Every request re-embeds the query and re-fuses both rankings, so there is no stable ordering to page through. Raise topK instead.
An empty hits array is a normal result, not an error.
Degraded results
When the embedding provider does not answer, the search falls back to keyword retrieval and says so:
degraded | Meaning |
|---|---|
| absent | Both retrievers ran |
lexical_only | Keyword retrieval only. The embedding call for the query failed |
partial_lexical_only | Some of the searched knowledge bases fell back to keyword retrieval |
Results are still returned with HTTP 200. Check the field if recall matters to you.
A refused call to a built-in embedding model is not a degraded result: the search answers with the refusal's status (402 when usage is used up) instead of lexical_only.
Citations
An agent that answers from a knowledge base cites the passages it used. Each citation carries the chunkId of the passage, and you can turn it back into text at any time:
GET /v1/knowledge-bases/{knowledgeBaseId}/citations/{chunkId}
{
"chunkId": "chk_…",
"documentVersionId": "ver_…",
"text": "Mounting. Tighten the four M8 bolts to 25 Nm in a cross pattern …",
"textStatus": "AVAILABLE",
"pageStart": 14,
"pageEnd": 14,
"headings": ["Installation", "Mounting"],
"isCurrentVersion": true
}
Citations are durable on purpose:
- After a re-ingest, a citation issued against the old revision still returns the full text the model saw, with
isCurrentVersion: false. - After the document is deleted, the citation still resolves with its ids, page range and headings, but the passage is withheld:
textisnullandtextStatusisWITHDRAWN. - A chunk id that does not exist, or belongs to another knowledge base or organization, is a
404.
How citations are produced, and the backbone:citation items an agent emits, are described on the Agents page.
Using a Knowledge Base with an Agent
Attach one or more knowledge bases to an agent by adding the built-in file_search tool to the agent's tools:
{
"type": "file_search",
"config": { "knowledgeBaseIds": ["kb_123", "kb_456"] }
}
The model can then search those knowledge bases during a turn. It cannot name a knowledge base itself: which ones it may search is your configuration on the agent version, not a request parameter. Searching across several knowledge bases fuses their rankings into one list.
In the console, the agent form's Tools section offers the same setting with a knowledge base picker. Details of the tool, its result shape and citation handles are on the Agents page.
Managing Knowledge Bases
| Operation | Endpoint | Notes |
|---|---|---|
| List | GET /v1/knowledge-bases | Paginated, newest first |
| Get | GET /v1/knowledge-bases/{id} | |
| Update | PATCH /v1/knowledge-bases/{id} | Takes the same body as create. Mutable fields are replaced and the result is snapshotted as the next configuration version. embeddingDim cannot change |
| Delete | DELETE /v1/knowledge-bases/{id} | Soft delete; releases the slug, keeps version history. Admin only |
| Get document | GET /v1/knowledge-bases/{id}/documents/{documentId} | The document with the revision retrieval serves |
| Delete document | DELETE /v1/knowledge-bases/{id}/documents/{documentId} | Takes every revision out of retrieval and withdraws the passage text from previously issued citations. Admin only |
The console
Open Knowledge in the sidebar.
- List shows name, status, embedding model, chunking strategy and creation date.
- Create derives the slug from the name, offers Built-in as the first embedding provider whenever the platform offers built-in embedding models (preselected when your organization has no embedding-capable provider of its own), offers the embedding model catalog for the chosen provider, and hides chunking under an advanced section.
- Documents tab: a dropzone that enforces the size limits and splits large batches, a table with status badges, and polling while any revision is still ingesting. A revision that has not moved for fifteen minutes is marked as possibly stalled.
- Playground tab: run a search against this knowledge base and see ranked passages with page, headings and the matched span. Use it to tell "the agent is misconfigured" apart from "the knowledge base does not contain the answer".
- Settings tab: edit name, slug, description, hybrid search, chunking parameters and the text-search language; provider (Built-in for a knowledge base on the built-in models), model and dimension are read-only.
Permissions
| Role | Can |
|---|---|
| Viewer | List, read, search, resolve citations |
| Member | Create and update knowledge bases, upload documents |
| Admin | Delete knowledge bases and documents |
Search results are restricted to passages the calling principal may see.
Errors
| Status | When |
|---|---|
400 | Embedding dimension does not match the model, a model the provider does not serve (the built-in provider included), builtin as rerankerProviderId, invalid chunking parameters, attach with a file whose purpose is not knowledge |
402 | A search on a built-in knowledge base whose embedding call was refused because usage is used up |
404 | Unknown knowledge base, document, revision, chunk or file |
409 | Slug already in use |
410 | Attach with a file that has been deleted |
413 | A file or request over the size limit |
Uploads answer 202 even when every file was rejected; read each entry's error.