All endpoints are prefixed with /api. Protected endpoints require an Authorization: Bearer <token> header containing a valid JWT.
Register a new user account.
Request body:
{ "email": "user@example.com", "password": "..." }Response: { "id", "email", "role", "created_at" }
Authenticate and receive a JWT token.
Request body:
{ "email": "...", "password": "..." }Response: { "access_token", "token_type": "bearer" }
Returns the current authenticated user's profile. Protected.
Create a new chat session. Protected.
Request body: { "title": "New Session" } (optional)
Response: { "id", "title", "created_at", "updated_at" }
List all sessions for the current user, ordered by updated_at descending. Protected.
Get a single session. Returns 404 if it does not belong to the current user. Protected.
Rename a session. Protected.
Request body: { "title": "New Title" }
Delete a session and all its messages. Cascades to session_documents. Protected.
Retrieve the full message history for a session. Protected.
Response: list of { id, role, message_type, content, citations, created_at }
The citations field is a JSON array of objects. Format depends on source:
- Corpus/Qdrant:
{ document_id, chunk_id, title, court, year, citation, score } - Uploaded/pgvector:
{ document_id, chunk_id, title, page_number, filename, score }
Send a message to the AI and receive an SSE stream response. Protected.
Request body: { "content": "What is Article 21?", "explicit_mode": "auto" }
explicit_mode values:
"auto"(default): LangGraph classifies intent via LLM"document": Force document_chat path regardless of question content"corpus": Force corpus_search path regardless of question content
Response: text/event-stream (SSE)
SSE event format:
data: {"content": "token..."} <- streamed per chunk
data: [DONE] <- signals stream complete
event: citations
data: [{"title": "...", ...}] <- only if citations exist
Attach a document to a session (for RAG context). Protected.
Request body: { "document_id": "uuid" }
Detach a document from a session. Protected.
Upload a private PDF for processing. Protected.
Request: multipart/form-data with file field (PDF only).
Response:
{
"id": "uuid",
"title": "filename.pdf",
"status": "uploaded",
"message": "Document uploaded and processing started"
}If the file hash matches an existing document, the existing record is returned immediately without re-processing.
Poll the processing status of a document. Protected.
Response: { "id", "title", "status", "summary", "error_message" }
Status values: uploaded -> processing -> ready | failed
Serve a document PDF inline (for both corpus cases and user uploads). Protected.
Returns application/pdf with Content-Disposition: inline. Corpus documents are accessible to any authenticated user. Uploaded documents require owner_id match.
Semantic search across the 46k Qdrant corpus.
Request body:
{
"query": "right to privacy",
"limit": 10,
"filters": {
"court": "Supreme Court",
"year_min": 2000,
"year_max": 2023
}
}Response: list of case cards with similarity score and metadata.
Retrieve full metadata for a specific corpus case.
Attach a corpus case to a new chat session. Creates the session and Document record if needed, then returns the session ID for navigation.
Response: { "session_id", "document_id" }
Note: Corpus cases are not re-processed through Celery. The Document record is created with status="ready" and the agent queries Qdrant directly using the document_id filter.
Alternative semantic search endpoint.
Search for cases by exact or partial case name.
Get detailed case information.
Find cases similar to a given case.
Ask a question directly against the corpus search service.
Returns server status and configuration. No authentication required.