Knowledge Base
The SendSeven Knowledge Base is an AI-powered system that stores your organization's documents, FAQs, and website content. When queried, it uses retrieval-augmented generation (RAG) to find relevant information and generate accurate answers with source citations.
Required Scopes
| Scope | Purpose |
|---|---|
knowledge_base:read | Search and list documents and FAQ entries |
knowledge_base:create | Create documents and folders; preview, start, cancel and re-run website crawls |
knowledge_base:update | Edit entries and resolve flags on FAQ entries |
knowledge_base:delete | Delete documents; merge FAQ entries |
knowledge_base:admin | Everything above, including website crawls (crawl routes accept knowledge_base:create or knowledge_base:admin) |
knowledge_base:manage | Approve or reject AI corrections, retry their analysis, revert FAQ revisions |
Key Capabilities
- AI-powered search -- ask natural language questions and receive synthesized answers
- Document management -- upload text documents, FAQs, and files
- Website crawling -- automatically import content from your website
- Folder organization -- organize documents into folders (each folder becomes a separate search corpus)
- Source scores -- every source carries a match score, so you can decide when an answer is safe to send
Pricing
Knowledge Base search is free. The AI work around it (answers written from the results, adding knowledge) is paid in AI credits at EUR 0.01 per credit on every plan that includes the Knowledge Base. AI credits are billed separately from messages, but included messages left over in a month also cover AI credits (see AI credits: invoice and included pool). There is no separate Knowledge Base allowance:
| Plan | Base fee | Included pool / month | Per AI credit beyond the pool |
|---|---|---|---|
| Professional | EUR 79 | 2,500 | EUR 0.01 |
| Scale | EUR 199 | 10,000 | EUR 0.01 |
| Enterprise | EUR 499 | 40,000 | EUR 0.01 |
What the Knowledge Base costs in AI credits:
| AI credits | |
|---|---|
A search (POST /knowledge-base/search, /kb in the inbox, bot and agent lookups) | Free |
| The AI answer written from the results | Charged by the AI work it does |
| Adding knowledge (crawl, upload, re-import) | 1 AI credit per 10 pages |
The Knowledge Base is not included on Basic or API Only. For the full price list, see AI Credits.
Search the Knowledge Base
Ask a natural language question and receive an AI-generated answer with source references.
POST /api/v1/knowledge-base/search
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | The question or search query |
limit | integer | No | Max source documents to consider (default: 5, max: 20) |
folder_id | string | No | Restrict search to a specific folder |
curl
curl -X POST "https://api.sendseven.com/api/v1/knowledge-base/search" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"query": "What is your return policy for electronics?",
"limit": 5
}'
Response
{
"query": "What is your return policy for electronics?",
"answer": "Electronics can be returned within 30 days of purchase in their original packaging. Refunds are processed within 5-7 business days after we receive the item.",
"citations": [
{"index": 1, "title": "Return Policy - Electronics", "url": "https://www.example.com/returns", "kind": "website", "score": 0.95, "excerpt": "Electronics may be returned within 30 days of purchase in original packaging..."}
],
"sources": [
{
"title": "Return Policy - Electronics",
"url": "https://www.example.com/returns",
"kind": "website",
"document_id": "8c1f2a3b-...",
"entry_id": null,
"score": 0.95,
"excerpt": "Electronics may be returned within 30 days of purchase in original packaging...",
"text": "Electronics may be returned within 30 days of purchase in original packaging..."
},
{
"title": "How long does a refund take?",
"url": null,
"kind": "faq_entry",
"document_id": null,
"entry_id": "3d9e7c10-...",
"score": 0.78,
"excerpt": "Refunds are processed within 5-7 business days...",
"text": "Refunds are processed within 5-7 business days..."
}
],
"source_count": 2
}
| Field | Notes |
|---|---|
answer | The AI answer, written only from the sources found. When nothing relevant is found, a short "could not find" message. |
sources[] | The passages the answer was written from, best match first. |
sources[].kind | website (a crawled page), document (an uploaded or written document) or faq_entry. |
sources[].title | The document title or, for an FAQ entry, its question. null when the document has no readable title. |
sources[].url | The public web address of a crawled page, else null. This is the only field to link to. |
sources[].alternate_urls | Only on crawled pages whose website declares language versions (hreflang): the declared versions on the same domain as {language: url}, for example {"en": "https://www.example.com/en/returns", "de": "https://www.example.com/de/retouren", "x-default": "https://www.example.com/returns"}. Use it to link your customer to the page in their language. Missing on other sources. |
sources[].content_language | Only on crawled pages: the language of the imported page, when known. |
sources[].document_id, sources[].entry_id | The Knowledge Base document or the FAQ entry the passage came from. |
sources[].score | How closely the passage matches the query, 0 to 1. |
sources[].excerpt | A short extract of the passage (text carries the same value for older clients). |
citations[] | The same passages, numbered (index) in the order they were given to the AI. |
source_count | Number of sources. |
Treat other keys in a source as internal and subject to change.
Before you send an answer to a customer automatically, check that source_count is greater than 0 and that the best score is high enough for your use case. Route weak matches to a human agent.
Errors: besides the common errors, 403 with detail.code trial_ai_credits_exhausted when a free trial has used up its AI credits (see Free trial limits), and 429 above 60 requests per minute.
Python
import requests
BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}
result = requests.post(
f"{BASE_URL}/knowledge-base/search",
headers=HEADERS,
json={"query": "How do I cancel my subscription?", "limit": 5},
).json()
print(f"Answer: {result['answer']}")
for source in result["sources"]:
print(f" Source: {source['title']} (score: {source['score']})")
JavaScript
const BASE_URL = "https://api.sendseven.com/api/v1";
const HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
};
const response = await fetch(`${BASE_URL}/knowledge-base/search`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
query: "What payment methods do you accept?",
limit: 3,
}),
});
const result = await response.json();
console.log(result.answer);
for (const source of result.sources) {
console.log(` Source: ${source.title} (score: ${source.score})`);
}
Manage Documents
List Documents
GET /api/v1/knowledge-base/documents
| Parameter | Type | Default | Description |
|---|---|---|---|
folder_id | string | Filter by folder | |
document_type | string | Filter by document type (e.g. faq, document, crawled) | |
search | string | Search by title or source URL | |
page | integer | 1 | Page number |
page_size | integer | 20 | Items per page (max 100) |
curl -X GET "https://api.sendseven.com/api/v1/knowledge-base/documents?page=1&page_size=20" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"
A document that could not be indexed has status: "failed" and a human-readable last_error (English). When the file itself is the cause, last_error_code names it: empty, no_text (scanned image without selectable text), pdf_encrypted, pdf_damaged, not_utf8, too_large, unsupported or unknown. Retrying the same file cannot succeed for these codes, so POST /knowledge-base/documents/{id}/retry returns 422 for them; upload a fixed file instead. For any other failure last_error_code is null and a retry may help.
Create a Document
POST /api/v1/knowledge-base/documents
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Document title |
type | string | Yes | faq or document |
content | string | Yes | Document content (plain text or HTML) |
folder_id | string | No | Target folder ID |
curl -X POST "https://api.sendseven.com/api/v1/knowledge-base/documents" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"title": "Product Warranty Information",
"type": "document",
"content": "All products sold by Acme Store come with a standard 12-month warranty. This warranty covers manufacturing defects and hardware failures under normal use.",
"folder_id": "folder_policies"
}'
Delete a Document
DELETE /api/v1/knowledge-base/documents/{document_id}
curl -X DELETE "https://api.sendseven.com/api/v1/knowledge-base/documents/doc_warranty_info" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
Folders
Folders organize documents. Each folder acts as a separate search corpus, allowing targeted search with the folder_id parameter.
List Folders
GET /api/v1/knowledge-base/folders
Requires knowledge_base:read. Returns the folder tree: items holds the root folders, each with nested children, and total is the number of folders.
| Field | Where | Description |
|---|---|---|
document_count | each folder | Documents stored directly in this folder (subfolders not included) |
faq_entry_count | each folder | FAQ entries stored directly in this folder, in any status (subfolders not included) |
document_total | top level | All documents of the workspace, including documents in no folder. Matches total of GET /documents without folder_id. |
faq_entry_total | top level | All FAQ entries of the workspace, including entries in no folder. Matches total of GET /faq-entries without folder_id. |
Because the totals also count items that sit in no folder, they can be higher than the sum of the per-folder counts. Conversation summaries are not part of the Knowledge Base: their folders are not in the tree, and they are never counted in document_count or document_total or listed by GET /documents.
curl "https://api.sendseven.com/api/v1/knowledge-base/folders" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
{
"items": [
{
"id": "folder_general",
"name": "General",
"slug": "general",
"parent_id": null,
"is_system": true,
"document_count": 12,
"faq_entry_count": 0,
"is_ai_searchable": true,
"children": []
},
{
"id": "folder_managed_faq",
"name": "Managed FAQ",
"slug": "managed-faq",
"parent_id": null,
"is_system": true,
"document_count": 0,
"faq_entry_count": 38,
"is_ai_searchable": true,
"children": []
}
],
"total": 2,
"document_total": 12,
"faq_entry_total": 38
}
Create a Folder
POST /api/v1/knowledge-base/folders
curl -X POST "https://api.sendseven.com/api/v1/knowledge-base/folders" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Product Documentation",
"description": "Technical documentation and user guides for all products"
}'
Website Crawl
Trigger a website crawl to automatically import content from a URL. Crawled pages land in the Websites system folder. Requires the knowledge_base:create scope (knowledge_base:admin is also accepted). Checking a crawl's status needs knowledge_base:read.
POST /api/v1/knowledge-base/documents/crawl
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Starting URL to crawl. End the URL with / to limit the crawl to that folder (example.com/de/ excludes example.com/desk). |
follow_links | boolean | No | Follow links and crawl multiple pages (default true). Set false to index only the single URL. |
page_limit | integer | No | Maximum pages when follow_links is true (1–2000, default 200) |
max_depth | integer | No | Maximum crawl depth when follow_links is true (1–10) |
title_prefix | string | No | Prefix added to imported document titles (max 100 chars) |
rate_limit | number | No | Seconds between requests (0.1–10.0) |
refresh | boolean | No | Delete existing documents in the crawl scope (the host of url, under its path) before crawling (default false) |
exclude_paths | string[] | No | Path prefixes to leave out (max 50, each starts with /, max 200 chars). See Exclude rules. |
exclude_query_params | string[] | No | Query parameters to leave out (max 20), as name or name=value. See Exclude rules. |
language | string | No | Content language to keep, for example de, en or pt-br. Used with translation_handling. |
translation_handling | string | No | keep_all (default) imports every language version. skip_translations skips pages that are translations of a page in language. Requires language. See Multilingual websites. |
The crawl stays on the host of url and under its path. The path is a plain prefix: https://docs.acme.com/help imports /help, /help/... and also /helpdesk. End the URL with / to limit the crawl to that folder: https://docs.acme.com/help/ imports /help/... and /help itself, but not /helpdesk. Re-crawls and refresh use the same scope. Exclude rules narrow that scope further.
curl -X POST "https://api.sendseven.com/api/v1/knowledge-base/documents/crawl" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"url": "https://docs.acme.com",
"follow_links": true,
"page_limit": 100,
"max_depth": 3,
"exclude_paths": ["/blog/archive"],
"exclude_query_params": ["print"]
}'
{
"job_id": "b3f1c2d4-5678-90ab-cdef-1234567890ab",
"status": "queued",
"message": "Website crawl job started. Use the poll_url to check progress.",
"poll_url": "/api/v1/knowledge-base/documents/crawl/{job_id}/status"
}
Crawls run asynchronously. Poll GET /api/v1/knowledge-base/documents/crawl/{job_id}/status for progress, or watch it in the SendSeven dashboard under Knowledge Base.
Exclude rules
Exclude rules apply to sitemap URLs, to every link the crawler follows and to every page before it is imported. They are plain prefixes and names, never regular expressions.
| Rule | Matches | Does not match |
|---|---|---|
exclude_paths: ["/en"] | /en, /en/, /en/pricing | /enterprise, /blog/en |
exclude_paths: ["/blog/archive"] | /blog/archive/2024/post | /blog/archived, /blog |
exclude_query_params: ["lang"] | any URL with ?lang=..., whatever the value | URLs without lang |
exclude_query_params: ["lang=en"] | ?lang=en (value is case-insensitive) | ?lang=de |
- Paths match on whole path segments and ignore upper and lower case. Each path must start with
/; a trailing/is ignored./on its own is rejected, because it would exclude the whole site. - Query parameters are given as
name(any value) orname=value(that value only). Names may contain letters, digits,_,.,-,[and](max 64 chars). - Duplicates are removed. You can send up to 50 paths and 20 query parameters.
Invalid rules return 422 with detail.error_code set to invalid_exclude_rule and a readable detail.message:
{
"detail": {
"error_code": "invalid_exclude_rule",
"message": "Excluding '/' would exclude the whole website."
}
}
The same error is returned for a malformed language, for translation_handling: "skip_translations" without a language, and for rules that would exclude the start URL itself (nothing would be imported).
Check a URL before crawling
POST /api/v1/knowledge-base/documents/crawl/preview
Requires knowledge_base:create (or knowledge_base:admin). Send {"url": "https://example.com"}. The response is always 200: valid tells you whether the URL can be crawled (otherwise error_code is one of invalid_url, invalid_hostname, dns_unresolvable, blocked, unreachable, timeout or http_error). It also reports the page title, the final URL after redirects, the sitemap (sitemap.page_count, estimated_pages) and your workspace page limit (max_pages_limit).
language_analysis describes the languages of the website and suggests crawl settings. It is null when the URL is invalid or the analysis did not finish in time; it never blocks the preview.
| Field | Description |
|---|---|
is_multilingual | true when at least two language versions are part of the crawl scope |
signals | How languages were detected: sitemap_hreflang, html_hreflang, path_prefix, subdomain, query_param, html_lang |
languages[] | One entry per language, sorted by page_count (highest first): code, page_count (pages in the sitemap, null without a sitemap), location (root, path_prefix, subdomain, query_param or other), path_prefix, host, query_param, is_default |
default_language | The site's default language (x-default, else the start page's declared language) |
tenant_language | Your workspace language |
recommended_language | The language SendSeven suggests keeping |
recommendation_reason | most_pages, tenant_language, default_language, only_language or null |
pages_without_counterpart | Pages in other languages that have no version in the recommended language. skip_translations keeps them, because they may hold unique information. |
already_scoped | true when the URL already covers one language (for example example.com/de). No settings are needed. |
recommended_settings | language, exclude_paths, exclude_query_params and translation_handling: "skip_translations", ready to send to the crawl endpoint. null when nothing needs to change. |
alternative_language | Set only when your workspace language was recommended although another language has more pages: that more complete language. Otherwise null. |
alternative_reason | most_pages when alternative_language is set, else null |
alternative_page_difference | How many more pages the alternative language has than the recommended one |
alternative_settings | Crawl settings for the alternative language, same shape as recommended_settings |
Subdomain languages (de.example.com) are reported for information only: a crawl never leaves the host of its start URL.
How the recommendation is chosen:
- Keep the language with the most pages. That version is usually the most complete one, so the least information is lost; the other versions are mostly translations of it.
- If your workspace language has at least 90% of the largest page count, keep your workspace language instead.
- Without page counts (no sitemap), keep your workspace language if the site offers it, else the site's default language.
The settings are always expressed as exclude rules plus skip_translations, never as a narrower start URL. Pages outside the language folders (imprint, legal pages) are therefore still imported.
Example for https://sendseven.com in an English workspace. Each language has its own prefix; the sitemap lists 451 German pages under /de, 421 English under /en, 325 French under /fr and further languages. English has more than 90% of the German page count, so English is recommended and German, with 30 more pages, is offered as the alternative (response shortened, signals illustrative):
{
"valid": true,
"final_url": "https://sendseven.com/",
"language_analysis": {
"is_multilingual": true,
"signals": ["sitemap_hreflang", "path_prefix"],
"languages": [
{ "code": "de", "page_count": 451, "location": "path_prefix", "path_prefix": "/de" },
{ "code": "en", "page_count": 421, "location": "path_prefix", "path_prefix": "/en" },
{ "code": "fr", "page_count": 325, "location": "path_prefix", "path_prefix": "/fr" }
],
"tenant_language": "en",
"recommended_language": "en",
"recommendation_reason": "tenant_language",
"pages_without_counterpart": 49,
"already_scoped": false,
"recommended_settings": {
"language": "en",
"exclude_paths": ["/ar", "/de", "/es", "/fr", "/it", "/pl", "/pt", "/tr"],
"exclude_query_params": [],
"translation_handling": "skip_translations"
},
"alternative_language": "de",
"alternative_reason": "most_pages",
"alternative_page_difference": 30,
"alternative_settings": {
"language": "de",
"exclude_paths": ["/ar", "/en", "/es", "/fr", "/it", "/pl", "/pt", "/tr"],
"exclude_query_params": [],
"translation_handling": "skip_translations"
}
}
}
Both options exclude the prefixes of every other language. skip_translations additionally skips pages in another language whose version in the kept language is reachable in the crawl scope, for example translated pages outside the language folders. Pages that exist only in another language and lie outside the excluded prefixes are kept; pages_without_counterpart (49 here) counts the pages in other languages that have no English version. In a German workspace the same site recommends German (recommendation_reason: "most_pages") with the alternative_settings shown above, and all alternative_* fields are null.
To use the suggestion, copy recommended_settings (or alternative_settings, if you prefer the more complete language) into the crawl request next to url.
Crawl status
GET /api/v1/knowledge-base/documents/crawl/{job_id}/status (scope knowledge_base:read) reports progress (status, total_pages, pages_crawled, pages_failed, documents_created, errors) and what the filters did:
| Field | Description |
|---|---|
excluded_by_rules | Pages left out because they matched an exclude rule |
skipped_translations | Pages skipped because their version in language is part of the crawl |
skipped_translation_samples | Up to 20 examples: {url, language, counterpart_url} |
language | The language this crawl keeps (null when none was chosen) |
translation_handling | keep_all or skip_translations (null for crawls without filters) |
filters_auto_applied | true when the exclude rules and language were applied automatically from the language recommendation (by the onboarding website scan) rather than chosen by a user |
alternative_language | For an automatically applied crawl: the more complete language that was not imported because your workspace language was recommended. null otherwise. |
alternative_page_difference | How many more pages alternative_language has than the imported language |
For a multilingual website, the website scan during onboarding applies the recommended language automatically. Its crawl reports filters_auto_applied: true; to switch to the alternative language, re-crawl with alternative_settings from a new preview.
{
"job_id": "b3f1c2d4-5678-90ab-cdef-1234567890ab",
"status": "completed",
"base_url": "https://example.com",
"pages_crawled": 118,
"documents_created": 118,
"excluded_by_rules": 342,
"skipped_translations": 12,
"skipped_translation_samples": [
{
"url": "https://example.com/blog/hello",
"language": "en",
"counterpart_url": "https://example.com/blog/hallo"
}
],
"language": "de",
"translation_handling": "skip_translations"
}
Re-crawl with saved rules
Exclude rules and language settings are saved with the crawled website and reused by every re-crawl, so you set them once.
GET /api/v1/knowledge-base/documents/crawled-domains(scopeknowledge_base:read) lists crawled websites. Each entry carriescrawl_filters(exclude_paths,exclude_query_params,language,translation_handling), ornullwhen none were set.POST /api/v1/knowledge-base/documents/recrawl(scopeknowledge_base:create, bodyhistory_id,mode, ...) andPOST /api/v1/knowledge-base/folders/{folder_id}/recrawl(scopeknowledge_base:create) accept the same four fields.
Each of the four fields is handled on its own:
| You send | Result |
|---|---|
| field omitted | the saved value is reused |
"exclude_paths": [] / "exclude_query_params": [] | the saved rules are cleared |
"language": null | the saved language is cleared |
| a new value | replaces the saved value and is saved for future re-crawls |
The combined result is validated like a new crawl, so it can return 422 invalid_exclude_rule too. After a successful re-crawl, pages that are now excluded or skipped as translations are removed from the Knowledge Base. To clean up a website that was imported in all languages, re-crawl it with language and translation_handling: "skip_translations".
Multilingual websites
Import one language of a multilingual website. A Knowledge Base answer draws on up to 5 sources. When several of them are translations of the same article, they crowd out the other pages that would have helped, and answers get worse. Your bots and AI agents answer in the customer's language anyway, so one language version is usually enough.
- Language under its own prefix (
example.com/de): crawlhttps://example.com/de/(the trailing/keeps/deskand similar paths out), or crawl the root and exclude the other prefixes. - Main language at the root without a prefix (
example.comin German, plus/en,/fr,/it): crawlhttps://example.comand exclude/en,/frand/it. German pages, including pages outside any language folder, are imported. - Language as a query parameter (
?lang=en): excludelang=en,lang=frand so on, keeping the value you want.
Add "language" and "translation_handling": "skip_translations" as a safety net for translations the exclude rules miss. A page is skipped only when both are true:
- its own language (from
hreflang, the declared page language or a language prefix) is known and differs fromlanguage, and - it links a version of itself in
language(hreflang) that is part of the same crawl.
Pages that exist only in another language, and pages whose language cannot be determined, are kept, so no unique content is lost.
Importing one language does not mean customers only get links in that language. The crawl stores the language versions a page declares (hreflang on the page or in the sitemap, same domain only). When a bot, Conversational Agent or AI-drafted reply cites a crawled page, the link points to the declared version in the reply language, falling back to a regional variant, then x-default, then the imported page. The search API returns these versions as sources[].alternate_urls. Pages imported earlier get them with their next re-crawl.
curl -X POST "https://api.sendseven.com/api/v1/knowledge-base/documents/crawl" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"exclude_paths": ["/en", "/fr", "/it"],
"language": "de",
"translation_handling": "skip_translations"
}'
If you keep all languages anyway, SendSeven still recognises pages that are translations of each other. When a question matches several language versions of the same page, the answer uses only the best matching one, so other relevant pages keep their place in the answer.
FAQ Entries
FAQ entries are the curated question-and-answer pairs in the Managed FAQ folder — the answers an Assistant works from most directly. Unlike uploaded documents they follow a review lifecycle (draft → published → needs_review → archived), and only published entries are retrievable. For the concepts behind this, see the Knowledge Base for Bots guide.
List FAQ entries
GET /api/v1/knowledge-base/faq-entries
| Parameter | Type | Description |
|---|---|---|
status | string | draft, published, needs_review, or archived. Accepts a comma-separated list (e.g. draft,needs_review) for a union. |
origin | string | manual, auto_conversation, migrated, or correction. |
folder_id | string | Restrict to one KB folder. |
flagged_only | boolean | Only entries with flagged_count > 0. |
review_only | boolean | The review queue: entries needing attention (draft/needs_review, or flagged). Overrides status/flagged_only. |
search | string | Substring match on question/answer. |
sort_by | string | uses, quality, flags, retrieved, created_at, updated_at, or sort_order. |
page / page_size | integer | Pagination (page_size max 200). |
Each entry includes its status, origin, question, answer, and the usage counters times_in_sent_reply (Sent), times_retrieved (Retrieved), duplicate_count (Sources — a provenance count, not a duplicate warning), and flagged_count. rag_file_name is null when the entry is not indexed for retrieval.
{
"items": [
{
"id": "faq_a1b2c3",
"status": "published",
"origin": "auto_conversation",
"question": "What is your return policy?",
"answer": "You can return any unused item within 30 days...",
"times_in_sent_reply": 12,
"times_retrieved": 47,
"duplicate_count": 3,
"flagged_count": 0,
"rag_file_name": "faq_a1b2c3.md"
}
],
"pagination": { "page": 1, "page_size": 50, "total": 1 }
}
Create FAQ entries
POST /api/v1/knowledge-base/faq-items
POST /api/v1/knowledge-base/faq-items/batch
Both need knowledge_base:create. Send question and answer, and optionally:
| Field | Default | Description |
|---|---|---|
folder_id | the Managed FAQ folder | Knowledge Base folder for the entry |
status | published | draft, published, needs_review or archived. Only published entries are retrievable. |
language | none | Language tag, for example de |
allow_duplicate | false | When false, an entry that nearly matches an existing one is rejected with 409 and {"duplicate_detected": true, "existing_entry_id": "...", "similarity": 0.93} |
The batch endpoint takes {"items": [...]} with 1 to 50 entries in the same shape and returns {"created_count": 2, "items": [...]} (201). It is all-or-nothing: if one entry is a near-duplicate, nothing is saved and the 409 body also carries item_index, the position of that entry in your list. Published entries become retrievable shortly after the call returns.
document_id is a legacy field that appends entries to an older compiled FAQ document. Leave it out. In a batch, set it on every item or on none; a mix returns 400.
Review queue
The review queue collects every open Knowledge Base task in one ordered list, so you can work through them one at a time: AI corrections waiting for a decision, flagged FAQ entries, entries that need review, drafts and documents that failed to process. Both endpoints are read-only and need knowledge_base:read. You act on an item with the existing endpoints listed below.
Queue summary
GET /api/v1/knowledge-base/review-queue/summary
{
"total": 47,
"corrections": 3,
"entries": 42,
"breakdown": { "draft": 40, "needs_review": 1, "flagged": 3 },
"failed_documents": 2,
"merge_execution_enabled": false
}
| Field | Notes |
|---|---|
total | corrections + entries + failed_documents. Use it for a badge. |
corrections | Corrections that need a person: proposed, failed, and open or analyzing ones that have not moved for 60 minutes (stalled). |
entries | Distinct FAQ entries that are draft or needs_review, or flagged. The same set as GET /knowledge-base/faq-entries?review_only=true. |
breakdown | Overlapping counts per reason. A flagged draft counts under both draft and flagged, so the parts can add up to more than entries. |
failed_documents | Knowledge Base documents with status failed. |
merge_execution_enabled | false while merging duplicates is preview-only (see Find and merge duplicate entries). |
GET /api/v1/knowledge-base/faq-entries/review-count (count + breakdown) still answers for older clients but is no longer part of the public API. Use the summary instead.
Queue items
GET /api/v1/knowledge-base/review-queue?page=1&page_size=50
page_size is at most 100. The order is fixed: corrections (oldest first), flagged entries (most flags first), entries that need review, drafts (oldest first), then failed documents (oldest first). Each FAQ entry appears once, even a flagged draft. pagination.total equals summary.total, and the response also carries the current summary.
{
"items": [
{
"id": "correction:5b0c...",
"type": "correction",
"reasons": ["correction_proposed"],
"created_at": "2026-10-20T09:12:00Z",
"correction": { "id": "5b0c...", "status": "proposed", "...": "..." },
"entry": null, "entry_context": null, "document": null
},
{
"id": "faq_entry:3d9e...",
"type": "faq_entry",
"reasons": ["flagged", "draft"],
"entry": { "id": "3d9e...", "status": "draft", "question": "...", "answer": "...", "...": "..." },
"entry_context": {
"flags": [{"source": "rating", "verdict": "wrong", "reason": "outdated", "comment": "...", "question": "...", "correction_id": null, "correction_status": null, "flagged_by_name": "Jane", "created_at": "..."}],
"draft_origin": "auto_conversation",
"duplicate_of_entry_ids": []
}
},
{
"id": "failed_document:9a1f...",
"type": "failed_document",
"reasons": ["ingestion_failed"],
"document": { "id": "9a1f...", "title": "Price list 2026.pdf", "document_type": "uploaded_doc", "folder_id": "...", "last_error": "...", "source_url": null, "updated_at": "..." }
}
],
"pagination": { "page": 1, "page_size": 50, "total": 47 },
"summary": { "total": 47, "...": "..." }
}
type | reasons | Act with |
|---|---|---|
correction | correction_proposed, routed_to_skill_suggestion, no_changes_proposed, partially_applied, analysis_failed, analysis_stalled | POST /kb-corrections/{id}/approve or /reject; for analysis_failed and analysis_stalled, retry the analysis. See Feedback loop. |
faq_entry | flagged, needs_review, draft | Edit or publish with PUT /knowledge-base/faq-items/{id}, delete it, or resolve flags. |
failed_document | ingestion_failed, deletion_failed | ingestion_failed: POST /knowledge-base/documents/{id}/retry or delete it (when document.last_error_code is set, retry returns 422: upload a fixed file). deletion_failed: delete it again (retry returns 409). |
entry_context.flags lists up to 5 reasons an entry was flagged, newest first: source is rating (a team member rated an answer that used the entry) or report. question is the customer question the AI answered, null after 90 days. draft_origin is manual, auto_conversation, migrated, correction or agent_gap. duplicate_of_entry_ids lists live entries with the same question, as merge candidates.
Retry a correction analysis
POST /api/v1/kb-corrections/{correction_id}/retry-analysis
Requires knowledge_base:manage. No request body. Re-runs the AI analysis of a correction that is failed, or open/analyzing without progress for 60 minutes (stalled). The correction goes back to open and the analysis starts again. Returns 202 with the correction; poll GET /api/v1/kb-corrections/{correction_id} until it is proposed (or applied in automatic mode).
| Status | detail.code | Meaning |
|---|---|---|
202 | Analysis restarted. | |
404 | correction_not_found | Unknown correction. |
409 | not_retryable | The correction is neither failed nor stalled. |
Correction statuses: open (reported, not analyzed yet), analyzing, proposed (waiting for review), applied, rejected, failed (analysis or apply failed; last_error says why).
Resolve flags
Clears an entry's flags after a reviewer has looked at it: sets flagged_count to 0 and writes an audit revision, without touching the content, status, or the corpus. Requires knowledge_base:update.
POST /api/v1/knowledge-base/faq-entries/{entry_id}/resolve-flags
Editing or re-publishing an entry clears its flags automatically, so this endpoint is only needed when you want to keep an entry as-is and just dismiss its flags.
Find and merge duplicate entries
Duplicate clusters (live entries whose normalised question matches) are candidates for merging into one:
GET /api/v1/knowledge-base/faq-entries/duplicate-clusters
POST /api/v1/knowledge-base/faq-entries/merge
| Field | Type | Default | Description |
|---|---|---|---|
winner_id | string | — | The entry that survives the merge. |
loser_ids | string[] | — | Entries folded into the winner, then archived. |
dry_run | boolean | true | Preview only. Set false to execute the destructive merge. |
keep_loser_questions_as_variants | boolean | true | Keep loser phrasings as retrievable alternate questions on the winner. |
dry_run: true (the default) previews the merge — the combined stats, the question variants that would be kept, and the RAG files that would be removed — and changes nothing.
Merge execution (dry_run: false) is gated off in production. While the server-side flag is disabled, an execute request returns 403 before anything is deleted — this is intentional, not a transient error. Until execution is enabled, use dry_run: true to preview, then consolidate by hand. Requires knowledge_base:delete.
Error Responses
Most errors return detail as a message string. See Error Handling for the response shapes.
| Status | detail | Description |
|---|---|---|
| 400 | message string | Invalid request, for example "Content is required for FAQ/manual documents" |
| 401 | "Invalid or expired API token" | Token is invalid or expired |
| 403 | "Missing required permission(s): ..." | Token lacks the required knowledge_base:* scope for this operation (see the scope table above). A 403 is also returned when merge execution is attempted while it is gated off |
| 403 | {"code": "feature_disabled", "feature": ...} | The Knowledge Base is not included in your plan |
| 404 | "Document not found", "Folder not found", "Crawl job not found" | Unknown ID, or it belongs to another workspace |
| 409 | "Folder is being deleted" | The target folder is being deleted |
| 413 | "Content exceeds ... bytes limit." | Edited document content is too large |
| 422 | {"error_code": "invalid_exclude_rule", "message": ...} | Invalid crawl exclude rules, language or translation handling (details) |
| 422 | list of field errors | Invalid request body |
| 429 | {"error": "Rate limit exceeded: ..."} (no detail wrapper) | Crawl, recrawl, crawl preview or search called too often (see Rate Limits) |
Next Steps
- Send Text Messages -- send answers to customers
- Webhook Events -- receive events when knowledge base is queried
- API reference -- every knowledge base endpoint with its request and response fields