message.comDevelopers

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

GET/api/v1/kb/documentsAuth: Bearer

Code samples

cURL
curl 'https://app.message.com/api/v1/kb/documents' \
  -H 'Authorization: Bearer YOUR_WORKSPACE_JWT'
JavaScript
const res = await fetch('https://app.message.com/api/v1/kb/documents', {
  headers: { Authorization: 'Bearer ' + token }
});
const { documents } = await res.json();
Python
import requests
r = requests.get(
    "https://app.message.com/api/v1/kb/documents",
    headers={"Authorization": f"Bearer {token}"},
)
documents = r.json()["documents"]
Ruby
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
<?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"];
One document
{
  "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

POST/api/v1/kb/ingest/urlAuth: Bearer

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.

FieldTypeDescription
urlrequiredstringStart URL, must be a public address (no IP literals, no .internal/.local hosts).
maxDepthoptionalintegerLink depth from the seed URL. 0 to 3, defaults server-side.
maxPagesoptionalintegerCap on pages crawled. 1 to 200.
200 OK
{ "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

GET/api/v1/kb/jobs/:idAuth: Bearer

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: [...] }.

200 OK
{
  "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

DELETE/api/v1/kb/documents/:idAuth: Bearer

Removes the document and its chunks from the vector store.

POST/api/v1/kb/retrieveAuth: Bearer

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.

Body
{
  "query": "How do I cancel a subscription?",
  "topK": 5
}
200 OK
{
  "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

CodeWhen
400 validation_errorPayload failed schema validation.
400 website_not_publicThe crawl URL resolves to a private, internal, or non-public address.
404 not_foundDocument or job does not exist in this workspace.
503 embedding_unavailableThe self-hosted embeddings service is unreachable; retrieval failed.

Common pitfalls

  • Treating ingest as synchronous. POST /api/v1/kb/ingest/url returns 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.