API reference
The CRTX backend is a FastAPI application exposing five routers:
/collections, /ingest, /query, /chat, /evals. All paths are relative to
your backend origin (Base URL).
- Auth: every endpoint requires
Authorization: Bearer <token>. See Authentication. - Interactive docs: FastAPI serves auto-generated OpenAPI at
Base URL/docs(Swagger UI) andBase URL/openapi.json(the raw spec — use it to generate typed clients). - Content type: JSON unless noted (
/ingest/ismultipart/form-data;/query/responds withtext/event-stream).
#Common status codes
| Code | Meaning |
|---|---|
200 | Success. |
401 | Missing/invalid/expired token. |
403 | Authenticated but not allowed (not owner / no access / needs admin / needs ingest permission). |
404 | Resource not found (or not visible to you). |
413 | Upload exceeds the 50 MB limit. |
415 | Unsupported file type. |
422 | Request body failed validation. |
502 | Upstream storage upload failed. |
503 | Redis/worker unavailable — ingestion disabled. |
#Collections
#POST /collections/
Create a collection.
// body
{ "name": "Q3 Financials" } // 1–200 chars, required
Returns the created collection row (id, name, config, user_id,
created_at, …). The new collection's Pinecone namespace equals its id.
#GET /collections/
List collections you own and collections shared with you. Shared entries
include "shared": true and your "permission" (query | ingest).
#DELETE /collections/{id}
Owner only. Deletes the collection, cascades child rows, and purges the entire
Pinecone namespace. Returns { "deleted": "<id>" }.
#GET /collections/{id}/config
Admin only (app_metadata.is_admin). Returns the retrieval config, merged over
defaults:
{ "chunk_size": 1000, "retrieval_strategy": "similarity" }
#PUT /collections/{id}/config
Admin only. Update the retrieval config.
// body
{
"chunk_size": 1000,
"retrieval_strategy": "similarity" // "similarity" | "mmr" | "threshold"
}
Returns the stored config.
#GET /collections/{id}/documents
List documents in a collection (owner or member). Each document includes an
open_url: a 1-hour signed URL for PDFs (from the documents bucket) or the
original URL for URL-sourced documents.
#DELETE /collections/{id}/documents/{document_id}
Owner only. Removes the original from storage, deletes the document's Pinecone
vectors (resolved via document_chunks), and deletes the row. Returns
{ "deleted": "<document_id>" }.
#Sharing
| Method | Path | Notes |
|---|---|---|
POST | /collections/{id}/shares | Owner only. Body { "permission": "query" | "ingest" }. Returns a share row with a share_token. |
GET | /collections/{id}/shares | Owner only. Returns { shares: [...], members: [...] }; members include resolved email. |
DELETE | /collections/{id}/shares/{share_id} | Owner only. Revokes a link. Returns { "deleted": "<share_id>" }. |
DELETE | /collections/{id}/members/{member_id} | Owner only. Removes a member and their chats in this collection. Returns { "removed": "<member_id>" }. |
POST | /collections/join/{share_token} | Accept an invite. Returns { collection, permission }, or { already_owner } / { already_member, permission }. |
Full walkthrough: Sharing & collaboration.
#Ingestion
#POST /ingest/
Upload a document. multipart/form-data.
- Query param:
collection_id(optional). - Form field:
file— PDF, DOCX, PPTX, XLSX, CSV, TXT, or MD. Max 50 MB. - Permission: owner, or member with
ingestpermission. - Errors:
415unsupported type,413too large,403no permission,502storage failure,503Redis unavailable.
curl -X POST "$BASE_URL/ingest/?collection_id=$CID" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@report.pdf"
Returns { "job_id": "<uuid>" }.
#POST /ingest/url
Ingest a public web page.
// body
{ "url": "https://example.com/page", "collection_id": "…" } // collection_id optional
The URL must be http/https and resolve to a public host (private/loopback/
.local are rejected). Returns { "job_id": "<uuid>" }.
#GET /ingest/jobs/{job_id}
Poll job status (only your own jobs). Returns the ingest_jobs row:
{
"job_id": "…", "collection_id": "…", "source": "report.pdf",
"status": "processing", // queued | processing | succeeded | partial | failed
"chunks_processed": 240, "chunks_total": 512,
"error_message": null, "created_at": "…", "updated_at": "…"
}
Details and semantics: Ingesting documents.
#Query (streaming)
#POST /query/
Run a RAG query. Responds with text/event-stream (SSE), not JSON.
// body
{
"question": "…", // 1–5000 chars, required
"collection_id": "…", // optional; empty = personal namespace
"session_id": "…" // optional; ties the exchange to a chat session
}
Access: if collection_id is set, you must own it or be a member (403 otherwise).
Response — SSE events (each block is event: <type>\ndata: <json>\n\n):
event: | data: |
|---|---|
metadata | { "sources": [...], "embedding_latency_ms": <int>, "retrieval_latency_ms": <int> } — sent before the first token |
token | { "token": "<str>" } — one per output token |
done | { "generation_latency_ms": <int>, "time_to_first_token_ms": <int> } |
error | { "message": "Generation failed" } — only if generation fails mid-stream |
Each source object:
{
"source": "report.pdf",
"chunk_index": 42,
"text": "…",
"score": 0.83,
"chunk_type": "text", // or "image_description"
"image_url": null // set for figure/scanned-page sources
}
After the stream ends, the exchange is persisted and scored asynchronously (see Evaluation). Consumer examples: Querying & chat.
#Chat & sessions
All paths are scoped to a collection; access requires owner or member.
| Method | Path | Body / notes | Returns |
|---|---|---|---|
POST | /chat/{collection_id}/sessions | — | New session row |
GET | /chat/{collection_id}/sessions | — | [{ id, title, created_at, updated_at }], newest first |
GET | /chat/{collection_id}/sessions/{session_id} | Session owner only | [{ id, role, content, sources, created_at }] |
PATCH | /chat/{collection_id}/sessions/{session_id} | { "title": "…" } (1–100 chars) | { updated, title } |
DELETE | /chat/{collection_id}/sessions/{session_id} | Session owner only | { deleted } |
GET | /chat/{collection_id}/members | — | [{ user_id, email, role }] (excludes you) |
POST | /chat/{collection_id}/sessions/{session_id}/share | { "target_user_id": "<uuid>" } | { shared: true, new_session_id } |
Notes:
- Sessions are per-user; you can only read/modify sessions you own.
- Sharing a session copies it (with messages + sources) to a target who must already be a member/owner of the collection.
#Evaluations
Access requires owner or member of the collection.
#GET /evals/{collection_id}/stats
Aggregated metrics for the dashboard:
{
"total_queries": 128,
"scored_queries": 124,
"avg_faithfulness": 0.91,
"avg_context_relevance": 0.86,
"avg_total_latency_ms": 2450,
"avg_retrieval_latency_ms": 180,
"avg_generation_latency_ms": 2200,
"trend": [{ "date": "2026-07-01", "avg_faithfulness": 0.89,
"avg_context_relevance": 0.84, "count": 40 }],
"worst_queries": [{ "id": "…", "question": "…", "faithfulness_score": 0.35,
"context_relevance_score": 0.40, "total_latency_ms": 3100,
"engine": "langchain", "created_at": "…" }]
}
Aggregation currently reads up to the most recent 500 eval rows.
#GET /evals/{collection_id}
Paginated raw eval records.
- Query params:
limit(1–200, default 50),offset(≥0, default 0). - Returns:
[{ id, question, answer, faithfulness_score, context_relevance_score, total_latency_ms, retrieval_latency_ms, generation_latency_ms, engine, retrieval_strategy, top_k, scorer_error, created_at }], newest first.
#Notes for API clients
- SSE, not chunked JSON.
/query/streamstext/event-stream. Don't callres.json()— read the body as a stream and split on\n\n. - Poll ingestion; don't block.
/ingest/*returns ajob_idimmediately. Poll/ingest/jobs/{job_id}until a terminal status. - Signed URLs expire. Document
open_urls are valid for one hour — fetch them on demand, don't cache long-term. - Generate a typed client from
Base URL/openapi.json(e.g.openapi-typescript,openapi-python-client) rather than hand-writing request code. - Everything is per-user. Access is enforced from the token's
sub; there is no cross-tenant listing. For headless/scoped access patterns, see Programmatic access.