API reference
Every FAQ & Knowledge Base endpoint — which are public, which need a session, the permission each admin route gates on, the two rate limits, and the four tables behind them.
Two surfaces. /api/faq is reader-facing and mostly public; /api/admin/faq
carries an explicit permission on every route. Both prefixes sit behind the
knowledge_base licence gate — see Install.
Conventions that will catch you out
The platform pins the HTTP status at 200 and puts the real outcome in the body. Read the body, always. A client that branches on the status code will treat a refused save as a successful one.
tags and relatedFaqIds are DataTypes.JSON. That is a native JSON column on
MySQL, where the driver returns a parsed array, and LONGTEXT on MariaDB, where
it returns a raw string. Every endpoint normalises both to an array before
answering, but a consumer reading the tables directly must handle either shape —
this is the usual cause of an integration that works in development and breaks in
production.
faqs is soft-deleted. Filter deletedAt if you query it yourself.
Reader endpoints
Public — no session required
GET /api/faq accepts page, limit (capped at 100), search, category and
pagePath. Supplying neither page nor limit returns every matching row as a
plain array — a backward-compatibility path, and one that will hurt on a large
library.
GET /api/faq/{id} answers 404 for an unpublished article, not 403. Whether
an id exists is not something an anonymous caller needs to learn. Related articles
are capped at ten and filtered to published ones, so the list can be shorter than
what was configured. helpfulCount is counted from the feedback table at read
time, not stored on the row.
POST /api/faq/search takes { query, category }. The query must be at least 2
characters; at most 50 results are returned, ordered by the article's order
field. Queries longer than 3 characters are recorded to faq_searches with their
result count. The route resolves no session, so the recorded userId is always
null. Full behaviour in How search behaves.
GET /api/faq/stats deliberately omits the submitter's name and email from the
recently-answered list. Those never leave the admin side.
Session required
| Route | Limit |
|---|---|
POST /api/faq/{id}/feedback |
20 per hour, per user |
POST /api/faq/question |
5 per 24 hours, per user |
Feedback is one row per user per article, enforced by a unique index; a repeat vote updates the existing row. A repeat vote with no comment keeps the previous comment. Comments are capped at 1,000 characters.
The question body is { email, question }. The email must be valid and at most 254
characters; the question is 10–500 characters. The stored submitter name is
taken from the account, not from the request.
Admin endpoints
Articles
GET /api/admin/faq accepts page, limit (capped at 100), search, category,
status (active, inactive or all), pagePath and tags. search matches
question, answer and category. tags is comma-separated and behaves as an
AND — an article must carry every tag listed.
The response carries a counts object with active, inactive and total. Those are
computed across every row matching your filters except the status filter, so the
header counters cannot contradict the tab you are on.
Bulk update copies an explicit whitelist: question, answer, image,
category, tags, status, order, pagePath, relatedFaqIds. Anything else
in data is ignored rather than written.
On create, order is auto-assigned as MAX(order) + 1 for the page path — under a
row lock inside a transaction, so two simultaneous creates cannot collide — only
when the field is absent. An explicit 0 is honoured as position zero.
Pages and ordering
GET /api/admin/faq/page reads the filesystem. It skips _- and .-prefixed
directories plus api, admin and auth, strips [locale], and filters out
/admin, /auth, /api, /error, /404 and /500.
POST /api/admin/faq/reorder takes { faqId, targetId, targetPagePath }. A null
targetId appends to the end; a supplied one inserts before that article.
PUT /api/admin/faq/page/status and DELETE /api/admin/faq/page match on
pagePath alone. Narrowing the admin list by category, tag or search does not
scope them. Re-enabling a page also republishes articles that were deliberately
unpublished, because the per-article status is not remembered.
Categories and tags
Both gate on view.faq.category. There is no separate tag permission.
Questions
The list accepts page, limit (default 25, max 100), status and search.
search matches the question text, the submitter name and the email — server-side,
so it reaches the whole table. The counts object counts the whole table too, so
the tab badges cannot contradict the list.
Answering is committed before either notification is attempted. A mail outage cannot lose the answer; the failure is logged instead.
Feedback
The list accepts page, limit (default 25, max 100), filter (all,
helpful, unhelpful, withComments) and search, which matches the comment
text and the article's question.
Analytics
Takes no parameters — it is not windowed, and never has been. The timeframe control on the dashboard windows one chart in the browser.
viewsOverTime is retained for compatibility and is not a view time-series: it
is SUM(views) bucketed by the month each article was created. Do not plot it. See
The Knowledge Base dashboard.
percentageChange in the feedback comparison is null when the previous month
recorded none of that verdict. Render that as "no comparison", never as a gain.
AI
All six require DEEPSEEK_API_KEY and return 500 without it. Input bounds: topic
500 characters, question 2,000, content 20,000 — exceeded, the request is refused
with a 400 stating both lengths before anything is billed. Details and the cost
warnings are in AI-assisted authoring.
Permission keys
| Key | Used by |
|---|---|
access.faq |
Dashboard, Manage and AI screens; the analytics endpoint |
view.faq |
Article list and detail, page list, page counts |
create.faq |
Article creation and all six AI endpoints |
edit.faq |
Article update, bulk update, reorder, per-page status |
delete.faq |
Article delete, bulk delete, per-page delete |
view.faq.category |
Category list and tag list |
access.faq.question |
The Questions screen and the operations queue entry |
view.faq.question |
The question list |
edit.faq.question |
Answering, and changing a question's state |
access.faq.feedback |
The Feedback screen |
view.faq.feedback |
Feedback lists |
create.faq.feedback |
Writing feedback through the admin API |
Tables
| Table | Holds |
|---|---|
faqs |
Articles. Soft-deleted. Indexed on category, pagePath, order, status. views is a lifetime counter with no event history |
faq_feedbacks |
One row per user per article, unique on the pair, with an optional comment |
faq_questions |
Reader questions — name, email, question, answer, and a PENDING/ANSWERED/REJECTED state |
faq_searches |
One row per recorded search: query, resultCount, category. userId is always null. Deleted outright rather than soft-deleted — these are analytics records |
There is no WebSocket channel and no cron job in this addon. Nothing here polls, schedules or streams.