Knowledge base Live
The knowledge base grounds AI replies in your own content. Post a URL, we crawl it (up to two levels deep, up to 200 pages by default), chunk the text, embed each chunk with our self-hosted embeddings model, and store the vectors in Postgres. Used by chat AI, ticket AI, and agent assist for retrieval-augmented generation.
Crawling is a queued job, not a synchronous response: POST /api/v1/kb/ingest/url returns a job ID immediately and the crawl runs in the background. Poll GET /api/v1/kb/jobs/:id for status. There is no sources resource: the crawled result is a document, and a document can later be adopted into an authored article.
Documents
A document is one crawled page or uploaded file, plus its chunk count and embedding model. Documents are workspace-scoped.
List documents
Code samples
curl 'https://app.message.com/api/v1/kb/documents' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT'const res = await fetch('https://app.message.com/api/v1/kb/documents', {
headers: { Authorization: 'Bearer ' + token }
});
const { documents } = await res.json();import requests
r = requests.get(
"https://app.message.com/api/v1/kb/documents",
headers={"Authorization": f"Bearer {token}"},
)
documents = r.json()["documents"]require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/kb/documents")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
http.request(req)
end
documents = JSON.parse(res.body)["documents"]<?php
$ctx = stream_context_create([
"http" => ["method" => "GET", "header" => "Authorization: Bearer $token"]
]);
$documents = json_decode(file_get_contents("https://app.message.com/api/v1/kb/documents", false, $ctx), true)["documents"];{
"id": "uuid",
"sourceType": "url",
"sourceUrl": "https://docs.acme.com/billing/cancel",
"title": "Cancelling your subscription",
"byteSize": 4213,
"chunkCount": 6,
"embeddingModel": "<opaque model identifier>",
"articleId": null,
"ingestedAt": "2026-05-13T15:00:00Z"
}The model identifier is redacted in this example. Treat embeddingModel as opaque metadata, not a fixed value to branch on.
Crawl a URL
Queues a crawl. Admin-only. There is no separate create-then-crawl step and no manual recrawl action: re-posting the same URL queues a fresh job and supersedes the prior document.
| Field | Type | Description |
|---|---|---|
| urlrequired | string | Start URL, must be a public address (no IP literals, no .internal/.local hosts). |
| maxDepthoptional | integer | Link depth from the seed URL. 0 to 3, defaults server-side. |
| maxPagesoptional | integer | Cap on pages crawled. 1 to 200. |
{ "jobId": "uuid", "status": "queued" }That is the entire response: a job ID and its starting status, not the job row. Fetch the full row with the endpoint below.
Check crawl status
Returns the job row directly (not wrapped in a job key). Poll status: queued, running, done, or failed (see error). GET /api/v1/kb/jobs lists the most recent 20 jobs for the workspace with no params, also unwrapped as { jobs: [...] }.
{
"id": "uuid",
"workspaceId": "uuid",
"kind": "url",
"status": "queued",
"input": { "url": "https://docs.acme.com", "maxPages": 200, "maxDepth": 2 },
"startedAt": null,
"finishedAt": null,
"error": null,
"stats": null,
"createdByAgentId": "uuid",
"createdAt": "2026-05-10T12:00:00Z"
}Delete a document
Removes the document and its chunks from the vector store.
Semantic retrieval
Embeds the query, runs cosine-similarity search over the vector store, returns the top-K chunks. This is the same call the AI pipeline makes internally; exposed here for custom UIs.
{
"query": "How do I cancel a subscription?",
"topK": 5
}{
"chunks": [
{
"chunkId": "uuid",
"documentId": "uuid",
"chunkIndex": 2,
"sourceUrl": "https://docs.acme.com/billing/cancel",
"title": "Cancelling your subscription",
"text": "To cancel your subscription, visit...",
"score": 0.82
}
],
"queryEmbedTimeMs": 42,
"searchTimeMs": 11
}Errors
| Code | When |
|---|---|
400 validation_error | Payload failed schema validation. |
400 website_not_public | The crawl URL resolves to a private, internal, or non-public address. |
404 not_found | Document or job does not exist in this workspace. |
503 embedding_unavailable | The self-hosted embeddings service is unreachable; retrieval failed. |
Common pitfalls
- Treating ingest as synchronous.
POST /api/v1/kb/ingest/urlreturns a job ID, not a ready document. Poll the job before expecting search results. - Pointing at a private or internal URL. The crawler rejects non-public hosts outright, including at every redirect hop.
- Searching without grounding. Retrieval returns chunks; combining those chunks with an LLM is the AI engagement. See RAG concept.