REST API

All API routes are under /api/v1/. Endpoints that require an API key expect it as a Bearer token: Authorization: Bearer cape_<key>.


Chat

POST /api/v1/chat

Stream a chat response grounded in the knowledge base. Responses are delivered as Server-Sent Events (SSE).

Auth: API key required

Request body:

{
  "message": "How do I upload an asset?",
  "source": "user_docs",
  "conversationId": "clx...",
  "language": "en",
  "retrieval": "standard",
  "stream": true
}
FieldTypeRequiredDescription
messagestringYesThe user's question
sourcestring | string[]NoNamespace(s) to search. Defaults to all.
conversationIdstringNoResume an existing conversation
languagestringNoBCP 47 code. Auto-detected if omitted.
retrievalstandard | smartNoRetrieval mode. Default: standard
streambooleanNoEnable SSE streaming. Default: true

SSE events:

data: {"type":"meta","conversationId":"clx...","sources":[{"title":"Upload guide","documentId":"..."}]}

data: {"type":"chunk","text":"To upload an asset, "}

data: {"type":"chunk","text":"navigate to the Assets section..."}

data: [DONE]

Conversations are persisted server-side. Pass conversationId from the meta event in subsequent requests to continue the same thread.


Search

POST /api/v1/search

Semantic search over the knowledge base. Returns ranked chunks without generating a response.

Auth: API key required

Request body:

{
  "query": "how does role-based access work",
  "source": ["tech_docs", "api_endpoints"],
  "topK": 5
}
FieldTypeRequiredDescription
querystringYesSearch query
sourcestring | string[]NoNamespace filter
topKnumberNoMax results (1–20). Default: 5

Response:

{
  "results": [
    {
      "chunkId": "...",
      "documentId": "...",
      "documentTitle": "Access control",
      "headingPath": "Role-based access > Permissions",
      "content": "...",
      "namespace": "tech_docs",
      "score": 0.91
    }
  ],
  "count": 1
}

score is cosine similarity (0–1). Higher is more relevant.


FAQ

GET /api/v1/faq

Generate a structured FAQ document for a topic. Results are cached for 7 days.

Auth: None (public)

Query parameters:

ParamRequiredDescription
qYesFAQ topic or question
sourceNoNamespace. Default: user_docs. Use all for all namespaces.
langNoBCP 47 language code

Example:

GET /api/v1/faq?q=asset+upload&source=user_docs&lang=en

Response:

{
  "query": "asset upload",
  "scope": "user_docs",
  "language": "en",
  "content": "# Asset Upload FAQ\n\n## How do I...",
  "cached": true
}

content is a Markdown string with 5–10 Q&A pairs. cached: true means the response was served from the database cache.


Ingestion

POST /api/v1/ingest/md

Ingest raw Markdown content.

Auth: API key required

Request body:

{
  "title": "Getting started",
  "content": "# Getting started\n\nWelcome to Cape...",
  "namespace": "user_docs",
  "slug": "getting-started",
  "metadata": { "version": "2.1" }
}
FieldTypeRequiredDescription
titlestringYesDocument title
contentstringYesRaw Markdown
namespacestringYesuser_docs, tech_docs, api_endpoints, or confluence
slugstringNoUnique key for deduplication. Defaults to title.
metadataobjectNoArbitrary key-value metadata

Response:

{ "documentId": "clx...", "skipped": false }

skipped: true means the content hash matched the existing document — no re-embedding was done.


POST /api/v1/ingest/url

Fetch a URL and ingest its content.

Auth: API key required

Request body:

{
  "url": "https://docs.cape.io/getting-started",
  "namespace": "user_docs",
  "title": "Getting started"
}

Returns the same shape as /ingest/md. Returns 422 if the URL cannot be fetched.


POST /api/v1/ingest/openapi

Ingest an OpenAPI (Swagger) spec. Creates one document per operation.

Auth: API key required

Content-Type: application/json or application/yaml

Request body: The raw OpenAPI spec JSON or YAML.

Response:

{ "upserted": 12, "unchanged": 3, "total": 15 }

Operations are keyed by operationId. Re-posting the same spec will skip unchanged operations.


POST /api/v1/ingest/confluence

Bulk-ingest an entire Confluence space. This is an async operation.

Auth: API key required

Request body:

{
  "spaceKey": "ENG",
  "namespace": "confluence"
}

Response (202 Accepted):

{ "jobId": "clx...", "status": "pending" }

Monitor progress via GET /api/v1/ingest/jobs. Requires CONFLUENCE_BASE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN environment variables.


GET /api/v1/ingest/jobs

List the last 100 ingestion jobs.

Auth: Admin (session or API key)

Response:

{
  "jobs": [
    {
      "id": "clx...",
      "type": "confluence",
      "status": "done",
      "payload": { "spaceKey": "ENG" },
      "completedAt": "2026-04-30T10:00:00Z"
    }
  ]
}

Statuses: pending · running · done · failed. Failed jobs include an error string.


API Keys

GET /api/v1/keys

List all API keys.

Auth: Admin (session or API key)

Response:

{
  "keys": [
    {
      "id": "clx...",
      "label": "Production",
      "requestCount": 1482,
      "rateLimitRpm": null,
      "rateLimitRpd": 5000,
      "createdAt": "2026-01-15T09:00:00Z",
      "lastUsedAt": "2026-04-30T08:43:00Z",
      "revokedAt": null
    }
  ]
}

POST /api/v1/keys

Create a new API key.

Auth: Admin (session or API key)

Request body:

{
  "label": "CI/CD pipeline",
  "rateLimitRpm": 60,
  "rateLimitRpd": 10000
}

Response (201 Created):

{
  "id": "clx...",
  "label": "CI/CD pipeline",
  "key": "cape_a3f1c9d2...",
  "createdAt": "2026-04-30T10:00:00Z"
}

The key field is returned only once. Store it immediately — it cannot be retrieved again.


DELETE /api/v1/keys/:id

Revoke an API key.

Auth: Admin (session or API key)

Response:

{ "revoked": true, "id": "clx..." }

Returns 404 if the key does not exist, 409 if it is already revoked.


Conversations

GET /api/v1/conversations

List stored conversations with their messages.

Auth: Admin (session or API key)

Query parameters:

ParamDescription
sourceFilter by namespace
languageFilter by language code
limitResults per page (max 200, default 50)
offsetPagination offset

Response:

{
  "conversations": [ { "id": "...", "messages": [...] } ],
  "total": 312,
  "take": 50,
  "skip": 0
}