The data model
A knowledge base holds entries. Each entry is one fact expressed as a question and a self-contained answer, filed under a short subject. Entries are what retrieval returns and what a flow's model reads — never the original document — so an answer must stand on its own.
{
"id": "…", "subject": "Warranty",
"question": "What is the warranty on the X200?",
"altQuestions": ["X200 warranty length", "How long is the X200 covered?"],
"answer": "Two years from the date of purchase, parts and labour.",
"keywords": ["X200", "warranty"],
"sourceExcerpt": "…the X200 carries a two-year warranty…", "page": 4,
"status": "active", "conflictOfId": null, "validUntil": null,
"hitCount": 3, "lastHitAt": "2026-08-29T07:30:00Z"
}| Field | Rules |
|---|---|
subject | 1–60 chars, Title Case, one or two words. Reuse what GET …/subjects returns rather than inventing near-duplicates. Part of the embedding, so changing it re-indexes. |
question | One clear question a customer would actually ask, ≤ 500 chars. Exact duplicates (case-insensitive) are rejected. |
altQuestions | Up to 5 paraphrases — different wordings or languages. This is how you cover variants without duplicate entries. |
answer | ≤ 2000 chars, complete. The model sees only matched entries. |
keywords | Up to 10 product codes, names, numbers that keyword search should hit. |
status | active (searchable) · draft (hidden until approved) · archived (retired). |
validUntil | For promotions and seasonal facts. Listed under status=expiring 30 days before. |
hitCount | How often retrieval returned it. sort=unused finds dead weight. |
A source is one piece of submitted material (an uploaded file or pasted text); entries point back to it with sourceId,page and sourceExcerpt. Deleting a source deletes its entries.
Retrieving: search vs ask
Retrieval is hybrid: vector similarity on the entry's embedding plus Postgres full-text on the words, fused into one ranking. Both endpoints take the same body — query, optional topK and minScore — and both count as read.
Free · no model call
POST …/search
Returns ranked entries with vectorScore (cosine, 0–1) and a fused score. Use it to iterate, to check for an existing entry before you add one, and whenever you want to compose the answer yourself.
Credits · one model answer
POST …/ask
Runs the search, then answers only from the hits (“I don't have that information.” when they don't cover it). This is exactly what a flow's Knowledge tool grounds on — the best way to test how the base behaves in production.
Reading the scores: vectorScore ≥ 0.6 is a strong match, 0.35–0.6 is related, below is noise (dropped unless full-text also matched). Every hit bumps the entry's hitCount; a query that clears nothing is logged under unanswered.
Growing a base
Three ways in, by what you have:
| You have… | Use | Cost |
|---|---|---|
| one fact | POST …/entries — stored verbatim, indexed immediately | embedding only |
| many ready pairs | POST …/import — up to 2000 rows, stored verbatim | embeddings only |
| raw text or a document | POST …/submissions — the AI reads it and extracts pairs | extraction + embeddings |
Ready-made Q&A does not go through submissions
Submissions run model extraction over your text: it costs credits and may reword or split your pairs. Entries and import store them exactly as sent.
AI extraction from text and files
Send clean text — headings and paragraphs, boilerplate stripped. 5–40 k characters per call gives the best pairs; up to 400 k is accepted and processed in parallel windows.
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/submissions -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{
"text": "## Returns policy\nItems can be returned within 30 days of delivery in original packaging. Refunds are issued to the original payment method within 5 working days of receipt. Sale items are exchange-only.\n\n## Shipping\nWest Malaysia: RM 8 flat, 2–4 working days. East Malaysia: RM 15, 4–7 working days. Free shipping above RM 150."
}'{
"status": "success",
"title": "Returns & Shipping Policy",
"summary": "Return window, refund timing, sale-item rule and domestic shipping rates.",
"entries": [
{ "id": "…", "subject": "Returns", "question": "How long do I have to return an item?", "answer": "30 days from delivery, in original packaging.", "status": "active" },
{ "id": "…", "subject": "Shipping", "question": "How much is shipping to East Malaysia?", "answer": "RM 15, 4–7 working days; free above RM 150.", "status": "active" }
],
"extraction": { "status": "done", "windows": 1, "pairsExtracted": 7, "pairsInserted": 7, "duplicates": 0, "conflicts": 0, "credits": 0.02 }
}For a document, upload it first together with its text — the API stores the original but does not parse PDFs or Office files — then submit with the source attached. Mark page boundaries in the text with lines --- page N --- so entries get page numbers.
# 1. upload the file together with its text (the API does not parse PDFs — send the text you extracted)
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/sources/upload -H "Authorization: Bearer $DJC_TOKEN" \
-F "file=@datasheet.pdf" -F "content=<datasheet.txt" -F "pageCount=12"
# → 201 { "id": "b7e1…", "kind": "file", "title": "datasheet", … }
# 2. extract from it — "text" is now an instruction, not material
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/submissions -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{ "text": "Focus on specifications, prices and warranty. Skip marketing copy.",
"attachments": [ { "sourceId": "b7e1…" } ] }'If the base has review mode on (reviewBeforeIndex), extracted pairs land as drafts; approve the good ones withPOST …/entries/bulk { action: "approve" }. Add ?stream=1 to a submission to receive progress events as NDJSON instead of waiting for the whole response.
Importing ready pairs
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/import -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{ "rows": [
{ "subject": "Shipping", "question": "Do you ship to Singapore?", "answer": "Not yet — Malaysia only.", "altQuestions": ["International shipping"] },
{ "question": "What are your office hours?", "answer": "Mon–Fri 9am–6pm." }
] }'
# → { "inserted": 2, "duplicates": 0, "conflicts": 0 }- Exact-duplicate questions (against the base and within the batch, case-insensitive) are skipped and counted in
duplicates. - Rows ≥ 92 % similar to an existing entry are stored as draft conflicts — see below.
- Everything else lands
active, even in review mode. MissingsubjectbecomesGeneral.
Curating
After extraction, read the returned entries and tidy: fix wording with PATCH, merge near-duplicates, and make subjects consistent with a bulk setSubject. Text edits re-embed automatically.
Duplicates and conflicts
Whenever a new entry is ≥ 0.92 cosine-similar to an existing one, it is stored as a draft with conflictOfId pointing at the older entry, and it stays out of search until you decide:
curl "https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries?status=conflicts" -H "Authorization: Bearer $DJC_TOKEN"
# each item has conflictOfId → the older entry it resembles
curl -X PATCH https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries/$NEW_ID -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" -d '{ "resolveConflict": "replace" }' # archive the old one, activate this
curl -X PATCH https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries/$NEW_ID -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" -d '{ "resolveConflict": "keep" }' # both stay, flag clearedClosing the gaps
GET …/unanswered lists real questions retrieval failed on — from flows, from the app's Test drawer and from this API. Write entries for them, then clear the list:
curl https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/unanswered -H "Authorization: Bearer $DJC_TOKEN"
# [ { "query": "do you ship to singapore", "count": 4, "lastAt": "…" } ]
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{ "subject": "Shipping", "question": "Do you ship to Singapore?", "answer": "Not yet — Malaysia only.", "altQuestions": ["International shipping"] }'
curl -X DELETE "https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/unanswered?query=do%20you%20ship%20to%20singapore" -H "Authorization: Bearer $DJC_TOKEN"Playbook
- Inspect first.
GET /bases, thenGET …/subjectsandGET …/entries?limit=50. Know the size and taxonomy before changing anything. - Search before you add. If a near-identical entry exists,
PATCHit. Two answers to one question make retrieval worse. - Bulk material → submissions; ready pairs → import. Never paste a whole document into one
answer. - One fact per entry; wordings go in
altQuestions. - Test like a flow does with
ask; iterate for free withsearch. - Keep
descriptionaccurate — the model uses it to decide when to search this base. - Don't invent subjects that exist under another spelling, archive or delete entries you haven't read, or change a description the user didn't ask for. Bases can only be deleted in the app.
Every endpoint, with parameters and samples: Knowledge reference.