Knowledge Base
The Knowledge Base service is hosted help documentation for end users. Define categories, sections, articles, and FAQs through the API or MCP tools. Minolith hosts the rendered help centre at kb.minolith.io/{slug} with theming and search, and exposes a site-key-authenticated public API for SPA consumption.
Base URL: https://api.minolith.io/v1/kb
Authentication: All authenticated endpoints require an API key via the Authorization header:
Authorization: Bearer mlth_your_api_key_here
The public API (see Public API) does NOT use a bearer token — it uses a domain-locked site key instead.
Pricing:
| Action | Credits |
|---|---|
| Knowledge base created | 3 |
| Category created | 2 |
| Section created | 1 |
| Article created | 3 |
| FAQ entry created | 1 |
| All updates, reads, deletes, publish/archive, search, site-key management | Free |
Core Concepts
Knowledge Base — Top-level container. A project can have multiple (e.g. one per product). Has a slug, optional publishing toggle, theme, and public site key.
Category — Top-level grouping for content (e.g. "Getting Started", "Billing"). Optional icon.
Section — Optional sub-grouping inside a category. Articles and FAQs can live directly in a category or inside a section.
Article — Markdown help article with title, body, excerpt, tags, and cross-referenced related articles. Has a status: draft, published, archived.
FAQ — Lightweight question/answer pair. Rendered as an accordion on the hosted page.
Public Site Key (psk_...) — Auto-generated when you create a KB. Required for every call to the public API. Revoke and rotate via dashboard or API.
Allowed domains (shared) — The public API enforces a domain allow-list, but it is not stored on the knowledge base. The same project-level list used by the Changelog widget and Feedback widget is applied to KB public API requests. Edit it under Changelog settings (/projects/:id/changelog/settings).
is_public applies only to the hosted page. When is_public is false the hosted page at kb.minolith.io/{slug} returns 404, but the public API is still available for SPA consumption. Use this to ship purely via API without the hosted page.
Limits
| Limit | Value |
|---|---|
| Slug | 200 chars, lowercase alphanumeric + hyphens, unique per project |
| Title | 500 chars |
| Description | 10,000 chars |
| Category name | 200 chars |
| Section name | 200 chars |
| Article title | 500 chars |
| Article body | 100,000 chars |
| Article excerpt | 500 chars |
| Article tags | 20, each max 100 chars |
| Article related_articles | 10 IDs |
| FAQ question | 500 chars |
| FAQ answer | 10,000 chars |
| Site key rate limit | 1-1000 requests/minute (default 100) |
| Bulk article operations | 20 per call |
ID Prefixes
| Prefix | Entity |
|---|---|
kb_ |
Knowledge base |
psk_ |
Public site key |
kbc_ |
Category |
kbs_ |
Section |
kba_ |
Article |
kbf_ |
FAQ |
Knowledge Bases
Create knowledge base
POST /v1/kb/ — [3 credits]
Request:
{
"title": "Studbook.farm Help Centre",
"slug": "studbook-help",
"description": "Everything you need to know...",
"is_public": false,
"theme": "minimal"
}
slug is required and must be unique within your project. theme is one of minimal, developer, brand, docs, bare — default minimal. is_public controls the hosted page only; the public API works either way once you have a site key.
Response (201):
{
"data": {
"id": "kb_a1b2c3d4e5f6",
"title": "Studbook.farm Help Centre",
"slug": "studbook-help",
"description": "Everything you need to know...",
"is_public": false,
"shared_allowed_domains": ["studbook.farm", "www.studbook.farm"],
"theme": "minimal",
"theme_settings": null,
"logo_path": null,
"site_key": "psk_x9y8z7w6v5u4t3s2r1q0p9o8n7m6",
"public_url": null,
"created_at": "2026-04-22 14:00:00",
"updated_at": "2026-04-22 14:00:00"
}
}
shared_allowed_domains is read-only on KB responses — it reflects the project-level list stored on Changelog settings. Update that list at /projects/:id/changelog/settings.
IMPORTANT: The site_key field is returned only on create and on revoke. Store it immediately — there is no API to retrieve the full key again.
List knowledge bases
GET /v1/kb/ — free
Supports ?is_public=true|false, plus standard cursor pagination (limit, cursor).
Each item also includes category_count, section_count, article_count, faq_count.
Get knowledge base
GET /v1/kb/:id — free
Update knowledge base
PUT /v1/kb/:id — free
All fields optional. Same shape as create. Changing slug breaks existing hosted URLs and public API URLs.
Delete knowledge base
DELETE /v1/kb/:id — free
Cascades all categories, sections, articles, FAQs, and site keys.
Site Key Management
Get the active site key
GET /v1/kb/:id/site-key — free
Returns only the key prefix and metadata — the full plaintext key is never returned after creation.
{
"data": {
"id": "psk_abc123def456",
"kb_id": "kb_a1b2c3d4e5f6",
"key_prefix": "psk_x9y8z7w6",
"rate_limit": 100,
"created_at": "2026-04-22 14:00:00",
"revoked_at": null
}
}
Revoke and regenerate
POST /v1/kb/:id/site-key/revoke — free
Revokes the current key immediately and issues a new one. The new plaintext key is returned once in the response — store it immediately.
{
"data": {
"id": "psk_newidxxx",
"site_key": "psk_newkeyxxxxxxxxxxxxxxxxxxxxxxxx",
"key_prefix": "psk_newkey",
"rate_limit": 100,
"created_at": "2026-04-22 14:05:00"
}
}
Update rate limit
PUT /v1/kb/:id/site-key — free
{ "rate_limit": 200 }
Accepted range: 1-1000 requests/minute per key.
Categories
Create
POST /v1/kb/:kb_id/categories — [2 credits]
{
"name": "Getting Started",
"description": "Intro to the app.",
"icon": "book",
"sort_order": 10
}
List
GET /v1/kb/:kb_id/categories — free
Get / Update / Delete
GET|PUT|DELETE /v1/kb/:kb_id/categories/:id — free
Deleting cascades sections, articles, and FAQs.
Sections
Create
POST /v1/kb/:kb_id/sections — [1 credit]
{
"category_id": "kbc_abc123",
"name": "Your First Animal",
"description": "Step-by-step for first-time users.",
"sort_order": 10
}
List
GET /v1/kb/:kb_id/sections?category_id=kbc_xxx — free
Get / Update / Delete
GET|PUT|DELETE /v1/kb/:kb_id/sections/:id — free
Deleting a section sets section_id on its articles/FAQs to null (they remain in the category).
Articles
Create
POST /v1/kb/:kb_id/articles — [3 credits]
{
"category_id": "kbc_abc123",
"section_id": "kbs_def456",
"title": "How to register a new animal",
"body": "# Registering a New Animal\n\n...",
"excerpt": "Step-by-step guide.",
"status": "draft",
"tags": ["animals", "getting-started"],
"related_articles": ["kba_xyz789"]
}
status is draft (default), published, or archived. Setting status to published stamps published_at on the server.
List
GET /v1/kb/:kb_id/articles — free
Supports ?category_id, ?section_id (or null/ungrouped for category-level articles), ?status (CSV), ?tag, plus cursor pagination. Body is omitted in list responses — use GET /:id for the full article.
Get / Update / Delete
GET|PUT|DELETE /v1/kb/:kb_id/articles/:id — free
Publish / Archive
POST /v1/kb/:kb_id/articles/:id/publish — free
POST /v1/kb/:kb_id/articles/:id/archive — free
Bulk
POST /v1/kb/:kb_id/articles/bulk — [3 credits each]
PUT /v1/kb/:kb_id/articles/bulk — free
Body: {"articles": [ { ...article}, ... ]}, up to 20 per call. All articles are validated before any are inserted — a validation failure on any one aborts the entire batch.
FAQ Entries
Create
POST /v1/kb/:kb_id/faqs — [1 credit]
{
"category_id": "kbc_abc123",
"section_id": "kbs_def456",
"question": "How do I reset my password?",
"answer": "Click **Forgot password** on the login page and check your inbox.",
"status": "published"
}
List / Get / Update / Delete / Publish
GET|PUT|DELETE /v1/kb/:kb_id/faqs/:id — free
POST /v1/kb/:kb_id/faqs/:id/publish — free
Search
GET /v1/kb/:kb_id/search?q=... — free
Matches on article titles, bodies, excerpts, FAQ questions, and FAQ answers. By default includes drafts (authenticated users can see their own drafts); add ?status=published to restrict.
{
"data": {
"articles": [
{
"id": "kba_xyz",
"type": "article",
"title": "How to register a new animal",
"excerpt": "...",
"category_id": "kbc_abc",
"section_id": "kbs_def"
}
],
"faqs": [
{
"id": "kbf_abc",
"type": "faq",
"question": "How do I reset my password?",
"excerpt": "Click **Forgot password**...",
"category_id": "kbc_abc",
"section_id": null
}
]
}
}
Public API
The public API lets your SPA fetch help content without exposing your Minolith API key.
Base URL: https://api.minolith.io/v1/kb/public/{slug}
Required header: X-Minolith-Site-Key: psk_...
Required request origin: The Origin or Referer header must match one of the project's shared allowed_domains entries (the list is managed under Changelog settings and shared across Changelog/Feedback/KB).
Rate limit: Configurable per site key (1-1000 req/min, default 100). Standard rate-limit headers are set.
All three checks must pass for any public API request:
X-Minolith-Site-Keyheader present, key exists and is not revoked- Origin/Referer matches the project's shared allowed-domain list (empty list = allow all)
- Rate limit not exceeded
A failure on any check returns a generic 403 Access denied (no info on which check failed) except rate-limit which is 429 Rate limit exceeded. Draft and archived content is never returned.
The KB's is_public flag is not checked here — the public API remains available even when the hosted page is disabled.
SPA example
const KB_SLUG = 'studbook-help';
const SITE_KEY = 'psk_a1b2c3d4e5f6...';
async function loadStructure() {
const r = await fetch(`https://api.minolith.io/v1/kb/public/${KB_SLUG}/structure`, {
headers: { 'X-Minolith-Site-Key': SITE_KEY },
});
if (!r.ok) throw new Error('Failed to load help centre');
return (await r.json()).data;
}
async function loadArticle(id) {
const r = await fetch(`https://api.minolith.io/v1/kb/public/${KB_SLUG}/articles/${id}`, {
headers: { 'X-Minolith-Site-Key': SITE_KEY },
});
return (await r.json()).data;
}
Public endpoints
GET /v1/kb/public/{slug}/structure Full navigation tree (categories → sections → article titles)
GET /v1/kb/public/{slug}/categories Categories with article + FAQ counts
GET /v1/kb/public/{slug}/categories/{id} Category detail with sections, articles, FAQs
GET /v1/kb/public/{slug}/articles/{id} Full article (only if published)
GET /v1/kb/public/{slug}/faqs/{category_id} FAQs for a category
GET /v1/kb/public/{slug}/search?q=term Search published content
Structure response
{
"data": {
"title": "Studbook.farm Help Centre",
"description": "Everything you need...",
"slug": "studbook-help",
"categories": [
{
"id": "kbc_abc",
"name": "Getting Started",
"icon": "book",
"sections": [
{ "id": "kbs_def", "name": "First Animal", "articles": [ { "id": "kba_xyz", "title": "How to...", "excerpt": "..." } ] }
],
"articles": [],
"faq_count": 3
}
]
}
}
Article response
Related articles are resolved to title + excerpt so the SPA can render "See also" without extra calls.
{
"data": {
"id": "kba_xyz",
"title": "How to register a new animal",
"body": "# Registering a New Animal\n\n...",
"excerpt": "...",
"tags": ["animals"],
"related_articles": [ { "id": "kba_def", "title": "CSV Import", "excerpt": "..." } ],
"category": { "id": "kbc_abc", "name": "Getting Started" },
"section": { "id": "kbs_def", "name": "First Animal" },
"published_at": "2026-04-22 14:00:00",
"updated_at": "2026-04-22 14:00:00"
}
}
Hosted Page
When is_public is true, a live help centre is served at https://kb.minolith.io/{slug}. It renders through one of five themes (minimal, developer, brand, docs, bare) and is cached in Redis; any write operation invalidates the cache.
When is_public is false, the hosted URL returns 404 but the API, MCP tools, and dashboard continue to work normally. Use this to build your content privately before launch, or to use the service purely as an internal API without a hosted page.
Theme settings
Set theme_settings on the knowledge base to customise appearance:
{
"primary_color": "#4f46e5",
"dark_mode": false,
"custom_css": ".kb-content h1 { letter-spacing: -0.02em; }"
}
primary_color must be a valid hex colour. custom_css max 50,000 chars. dark_mode applies to the brand theme.
Error Codes
| Code | HTTP | Meaning |
|---|---|---|
kb_not_found |
404 | Knowledge base not found in this project. |
category_not_found |
404 | Category not found. |
section_not_found |
404 | Section not found. |
article_not_found |
404 | Article not found. |
faq_not_found |
404 | FAQ not found. |
site_key_not_found |
404 | No active site key for this knowledge base. |
validation_error |
422 | Field-level validation failure. |
forbidden |
403 | Public API: site key invalid, revoked, or domain not allowed. |
rate_limit_exceeded |
429 | Too many requests for this site key. |
spending_cap_exceeded |
402 | Account monthly cap would be exceeded. |