Skip to content

Managing the Knowledge Base

The knowledge base (KB) stores question and answer pairs that the assistant references when composing replies. When a customer asks a question, the orchestrator searches the KB for relevant entries and uses them to ground the response. You can manage entries through the dashboard, but the API is the right choice when you need to import data in bulk, sync from another system, or automate content updates.

Every entry is scoped to a workspace. Create, list, search, and pending endpoints are addressed by workspace id; update, approve, reject, and delete are addressed by entry id.

Required scope: kb:read for read endpoints, kb:write for write endpoints.

ScenarioApproach
Add or edit a few entries manuallyUse the oHallo dashboard
Import hundreds of question and answer entries from a spreadsheetUse the API with a script
Sync product documentation from your CMS on a scheduleUse the API in a cron job
Approve or reject learning loop proposalsUse the API or the dashboard
Keep KB entries in sync with your help centreUse the API with polling

Each entry has a question, an answer, and a sourceType. You can optionally tag it with topics for categorisation. Entries created via the API are set to approved status by default.

Terminal window
curl -s -X POST "https://api.ohallo.eu/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/kb-entries" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \
-H "Content-Type: application/json" \
-d '{
"question": "What is the return policy for electronics?",
"answer": "Electronics can be returned within 30 days of purchase. Items must be in original packaging with all accessories. Opened software and digital products are non-refundable. Refunds are processed within 5 business days.",
"sourceType": "manual",
"topics": ["returns", "electronics"]
}' | jq .

Response (201 Created):

{
"id": "11223344-5566-4788-99aa-bbccddeeff00",
"workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "What is the return policy for electronics?",
"answer": "Electronics can be returned within 30 days of purchase...",
"topics": ["returns", "electronics"],
"sourceType": "manual",
"status": "approved",
"confidence": null,
"usageCount": 0,
"lastUsedAt": null,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:00:00.000Z"
}
FieldRequiredDescription
questionYesThe question this entry answers. Used for relevance matching.
answerYesThe answer content. This is what the assistant reads and uses in replies.
sourceTypeYesOrigin of the entry, e.g. "manual" for entries you create.
topicsNoList of topic tags for categorisation.
sourceConversationIdNoThe conversation an entry was derived from, when relevant.
confidenceNoA score between 0.0 and 1.0.

Writing effective content: Be specific and factual. Include concrete details like time periods, amounts, and conditions. Vague entries like “We have a flexible return policy” give the assistant nothing useful to work with.

List all entries for a workspace. The list endpoint returns a bare JSON array:

Terminal window
curl -s "https://api.ohallo.eu/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/kb-entries" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" | jq .

Search by semantic similarity with the q query parameter:

Terminal window
curl -s "https://api.ohallo.eu/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/kb-entries/search?q=return%20policy" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" | jq .

Both endpoints return a bare array:

[
{
"id": "11223344-5566-4788-99aa-bbccddeeff00",
"workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "What is the return policy for electronics?",
"answer": "Electronics can be returned within 30 days of purchase...",
"topics": ["returns", "electronics"],
"sourceType": "manual",
"status": "approved",
"usageCount": 0,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:00:00.000Z"
}
]

Update an existing entry by its id. Only question, answer, and topics can be changed, and only the fields you include are modified:

Terminal window
curl -s -X PATCH "https://api.ohallo.eu/api/kb-entries/11223344-5566-4788-99aa-bbccddeeff00" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \
-H "Content-Type: application/json" \
-d '{
"answer": "Electronics can be returned within 30 days of purchase. Items must be in original packaging with all accessories. Opened software and digital products are non-refundable. Refunds are processed within 3 business days via the original payment method."
}' | jq .

The response is the full updated entry.

When the learning loop resolves a conversation and detects a knowledge gap, it creates a proposal: a suggested KB entry with status set to pending_review. Proposals are not active until a human approves them.

List pending proposals for a workspace. This endpoint also returns a bare array:

Terminal window
curl -s "https://api.ohallo.eu/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/kb-entries/pending" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" | jq .
[
{
"id": "aabbccdd-eeff-4788-9233-445566778899",
"workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "What is the warranty on refurbished items?",
"answer": "Refurbished items carry a 12-month warranty from the date of purchase. The warranty covers manufacturing defects but does not cover cosmetic damage or battery degradation.",
"topics": ["warranty"],
"sourceType": "extracted",
"sourceConversationId": "c9f8e7d6-b5a4-3210-fedc-ba9876543210",
"status": "pending_review",
"confidence": 0.78,
"createdAt": "2026-03-25T14:00:00.000Z",
"updatedAt": "2026-03-25T14:00:00.000Z"
}
]

Approve a proposal with a POST to the approve endpoint:

Terminal window
curl -s -X POST "https://api.ohallo.eu/api/kb-entries/aabbccdd-eeff-4788-9233-445566778899/approve" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" | jq .

Reject a proposal with a POST to the reject endpoint, optionally including a reason:

Terminal window
curl -s -X POST "https://api.ohallo.eu/api/kb-entries/aabbccdd-eeff-4788-9233-445566778899/reject" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \
-H "Content-Type: application/json" \
-d '{
"reason": "This information is incorrect; refurbished items have a 6-month warranty, not 12 months."
}' | jq .

The rejection reason feeds back into the learning loop, so future proposals avoid the same mistake.

Delete an entry by its id:

Terminal window
curl -s -X DELETE "https://api.ohallo.eu/api/kb-entries/11223344-5566-4788-99aa-bbccddeeff00" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" | jq .

Response (200 OK):

{ "ok": true }

Example: syncing FAQ data from a JSON file

Section titled “Example: syncing FAQ data from a JSON file”

Here is a complete TypeScript script that reads FAQ entries from a JSON file and syncs them to your KB. It creates new entries and updates existing ones based on a matching [FAQ-<id>] marker embedded in the question text.

import { readFileSync } from 'fs'
const API_BASE = 'https://api.ohallo.eu'
const API_KEY = process.env.OHALLO_API_KEY
const WORKSPACE_ID = process.env.OHALLO_WORKSPACE_ID
if (!API_KEY || !WORKSPACE_ID) {
console.error('Set OHALLO_API_KEY and OHALLO_WORKSPACE_ID environment variables')
process.exit(1)
}
interface FaqEntry {
id: string
question: string
answer: string
}
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
}
// The list endpoint returns a bare JSON array of entries.
async function fetchExistingEntries(): Promise<Array<{ id: string; question: string }>> {
const res = await fetch(`${API_BASE}/api/workspaces/${WORKSPACE_ID}/kb-entries`, { headers })
if (!res.ok) throw new Error(`Failed to fetch entries: ${res.status}`)
return (await res.json()) as Array<{ id: string; question: string }>
}
async function createEntry(faq: FaqEntry): Promise<void> {
const res = await fetch(`${API_BASE}/api/workspaces/${WORKSPACE_ID}/kb-entries`, {
method: 'POST',
headers,
body: JSON.stringify({
question: `[FAQ-${faq.id}] ${faq.question}`,
answer: faq.answer,
sourceType: 'manual',
topics: ['faq'],
}),
})
if (!res.ok) throw new Error(`Failed to create entry FAQ-${faq.id}: ${res.status}`)
console.log(`Created: FAQ-${faq.id}, ${faq.question}`)
}
async function updateEntry(entryId: string, faq: FaqEntry): Promise<void> {
const res = await fetch(`${API_BASE}/api/kb-entries/${entryId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
question: `[FAQ-${faq.id}] ${faq.question}`,
answer: faq.answer,
}),
})
if (!res.ok) throw new Error(`Failed to update entry FAQ-${faq.id}: ${res.status}`)
console.log(`Updated: FAQ-${faq.id}, ${faq.question}`)
}
async function sync() {
const faqs: FaqEntry[] = JSON.parse(readFileSync('faqs.json', 'utf-8'))
const existing = await fetchExistingEntries()
let created = 0
let updated = 0
for (const faq of faqs) {
const marker = `[FAQ-${faq.id}]`
const match = existing.find((e) => e.question.startsWith(marker))
if (match) {
await updateEntry(match.id, faq)
updated++
} else {
await createEntry(faq)
created++
}
}
console.log(`Sync complete: ${created} created, ${updated} updated`)
}
sync().catch((err) => {
console.error('Sync failed:', err.message)
process.exit(1)
})

Example faqs.json input file:

[
{
"id": "001",
"question": "What are your business hours?",
"answer": "Our support team is available Monday to Friday, 9:00 to 18:00 CET. We respond to emails within 4 business hours."
},
{
"id": "002",
"question": "How do I reset my password?",
"answer": "Click 'Forgot password' on the login page. Enter your email address and follow the link in the reset email. The link expires after 1 hour."
},
{
"id": "003",
"question": "Do you ship internationally?",
"answer": "We ship to all EU countries and the UK. Standard delivery takes 3 to 5 business days within the EU. Express delivery, 1 to 2 business days, is available for an additional fee."
}
]

Run the sync:

Terminal window
OHALLO_API_KEY=sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx \
OHALLO_WORKSPACE_ID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
npx tsx sync-faqs.ts