Knowledge base
An agent with no knowledge base answers from what the model happens to remember about your business, which is nothing. Attach one and it answers from your documents instead — searched on every turn, inside the same turn, without the caller waiting for it.
When you need one
The symptom is specific: the agent is confidently wrong about your prices, your policies, your opening hours or your product range. No amount of prompt-writing fixes that, because you are asking a model to recall something it never knew. If instead the agent is answering in the wrong order, that is a flow problem, and if it needs a fact only your database has — this caller's order status — that is a function.
Step 1 · Create a base
/v1/knowledge_basescurl -s -X POST https://voice.sphoro.com/v1/knowledge_bases \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Clinic policies", "language": "en"}'{
"id": "kb_1e5ba0d984f7665b",
"name": "Acme Clinic policies",
"language": "en",
"document_count": 0,
"created_at": "2026-09-08T09:20:11Z"
}| Field | Type | Description |
|---|---|---|
namerequired | string | What you call it. |
descriptionoptional | string | Yours. |
languageoptional | string | The language the documents are written in. Worth setting: it is used when the documents and the caller are not in the same language. |
project_idoptional | string | The cost centre ingestion is attributed to. See projects. |
metadataoptional | object | String keys to string values. Yours. |
Step 2 · Put documents in it
/v1/knowledge_bases/{id}/documentscurl -s -X POST https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/documents \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Cancellation policy",
"text": "Appointments cancelled less than 24 hours before the slot are charged a late cancellation fee of 500 rupees. Cancellations more than 24 hours ahead are free."
}'curl -s -X POST https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/documents \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://acme.example/policies/cancellations"}'curl -s -X POST https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/documents \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://acme.example/help", "max_pages": 200}'# A PDF or DOCX, base64 in "content", with the real filename.
curl -s -X POST https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/documents \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"filename\": \"handbook.pdf\",
\"content\": \"$(base64 < handbook.pdf | tr -d '\n')\"
}"| Field | Type | Description |
|---|---|---|
textoptional | string | The document, inline. One of text,
content or url is required. |
contentoptional | string, base64 | A file's bytes. Send
filename with it — the extension is how the right extractor is
chosen. |
filenameoptional | string | The file's real name, including its extension. |
urloptional | string | A page to fetch, or the entry point for a crawl when
max_pages is set. |
max_pagesoptional | integer | Turns a single fetch into a crawl. Defaults to
200 pages and a depth of 3. robots.txt is honoured and each host is
rate-limited. |
titleoptional | string | Shown in retrieval results and prefixed onto every chunk. Supply one for inline text — it is a large part of what makes a chunk findable. |
languageoptional | string | Overrides the base's language for this document. |
metadataoptional | object | String keys to string values, returned with every chunk retrieved from this document and filterable at search time. |
Ingestion is a queued job
The request answers 202 Accepted and the work happens on a worker — a
two-hundred-page crawl is not something to hold an HTTP connection open for. Poll the document
list until the documents appear, or subscribe to
knowledge_base.document.ingested.
/v1/knowledge_bases/{id}/documentscurl -s https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/documents \
-H "Authorization: Bearer $SPHORO_API_KEY"202 and the last document landing, the agent is
grounded against a partial corpus and will answer as though the missing pages do not exist —
which reads as the agent being wrong rather than the ingest being unfinished.What the extractors will and will not do
Each of these will otherwise surprise you at the worst possible moment.
| Source | What you get |
|---|---|
| A scanned PDF | Nothing. It is a picture of a document. Ingestion succeeds with zero chunks, which is accurate rather than a failure — check the document count. |
| A multi-column PDF | Text in reading order, which is not always visual order. Academic papers and complex tables come out worst. |
| A DOCX with tracked changes | The accepted text. Comments and rejected revisions are ignored. |
| A JavaScript-rendered page | Whatever is in the served HTML. There is no headless browser, so a single-page app usually yields its loading state. |
| A crawl that hits its budget | It stops at max_pages and tells you how
many it saw. It will not exceed the budget to finish a site. |
Step 3 · Attach it to the agent
/v1/agents/{id}curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"knowledge_base_ids": ["'"$KB_ID"'"]}'search_knowledge_base in tools
does nothing on its own — the tool exists because a base is attached. Send [] to
detach every base. Several bases attached at once are searched together on each turn.Step 4 · Check what it retrieves, before a caller does
Search the base directly with the question you are worried about. This is the single most useful thing on this page: it turns "the agent gave a wrong answer" into "the right chunk is not being retrieved" or "the right chunk is retrieved and the prompt ignored it", which are different problems with different fixes.
/v1/knowledge_bases/{id}/searchcurl -s -X POST https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/search \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "what happens if I cancel the day before", "top_k": 5}'{
"results": [
{
"text": "Cancellation policy › Late cancellations — Appointments cancelled less than 24 hours before the slot are charged a late cancellation fee of 500 rupees.",
"title": "Cancellation policy",
"section": "Late cancellations",
"document_id": "doc_7c31f0a2",
"score": 0.81,
"vector_score": 0.78,
"lexical_score": 0.86
}
]
}| Field | Type | Description |
|---|---|---|
queryrequired | string | Ask it the way a caller would, not the way your documents are written. That gap is what you are testing for. |
top_koptional | integer | How many chunks to return. |
filteroptional | object | Restrict to documents whose metadata
matches — the way to search one product line inside a shared base. |
Both scores are returned because they fail differently. A high lexical_score with
a low vector_score means the caller happened to use your document's words; the
reverse means the meaning matched but the wording did not. A question that scores low on both is
one your corpus does not answer, however confidently the agent replies.
How a document becomes an answer
document ──▶ extract ──▶ chunk ──▶ embed ──▶ index
│
caller's question ──▶ embed ──▶ search ──▶ rank ──▶ floor ──▶ token budget
│
▼
grounding block in the promptChunking is structure-aware rather than a fixed character window: whole paragraphs, lists and
tables are packed together up to a budget, sentences are never split, and a chunk resets at a
heading so it cannot straddle two topics. Each chunk is then prefixed with its document title
and section path — which is the step that matters most and is the easiest to underestimate. A
chunk reading "₹500" is unmatchable by anybody; the same chunk stored as
"Cancellation policy › Late cancellations — a late cancellation is charged
₹500" is found by somebody asking about cancellation fees.
Retrieval inside a live call
Grounding happens inside the turn, which puts it squarely inside the latency budget. Two consequences worth designing around:
- It is bounded.
retrieval_timeout_mscaps it — 50 to 2000ms, underautonomy: "custom". See agent reference. - It fails open. A knowledge base that is slow or unreachable costs the caller a less-grounded reply, never silence on a live call. This is deliberate: grounding improves an answer, it is not a precondition for one.
Writing documents that retrieve well
| Do | Because |
|---|---|
| Give every document a real title, and use headings. | Both are prefixed onto chunks, and they are most of what makes a chunk findable. |
| Write the question your caller asks, not only the answer. | "What happens if I cancel late?" in the document matches "what if I cancel late" from a caller. A heading reading "Clause 4.2" matches nothing. |
| Use your callers' vocabulary alongside your own. | Somebody says "doctor", your handbook says "physician". Put both in. |
| Keep one topic per section. | Chunks reset at headings, so a section covering three things produces chunks that answer none of them well. |
| Split a 200-page handbook into documents by subject. | Filtering and deleting are both per-document, and a corpus you cannot prune is one you stop trusting. |
Keeping it current
/v1/knowledge_bases/{id}/documents/{doc}/v1/knowledge_bases/{id}curl -s -X DELETE https://voice.sphoro.com/v1/knowledge_bases/$KB_ID/documents/$DOC_ID \
-H "Authorization: Bearer $SPHORO_API_KEY"There is no in-place edit of a document: replace it by deleting and re-ingesting. Re-crawling a site adds documents rather than reconciling them, so a base fed by a nightly crawl wants the old documents removed first — otherwise last month's prices are still in there, still retrievable, and still perfectly confident.