The Agentic RAG API follows a multi-tenant, role-based access control (RBAC) model. Three user roles define permissions:
| Role | Company Affiliation | Permissions |
|---|---|---|
super_admin |
None (platform operator) | Full cross-company access; can create/manage companies, admins, and employees; cannot be created/promoted via API |
admin |
Single company | Manage users and documents within their company; can use chat |
employee |
Single company | Chat access only; no user/document management |
The database enforces an invariant at the CHECK constraint level:
super_adminusers must havecompany_id = NULLadminandemployeeusers must havecompany_id != NULL
This prevents accidental cross-role assignment at the database level.
Access: Public (no JWT required)
Authenticate a user and receive a JWT access token.
Request:
Content-Type: application/x-www-form-urlencoded
username=alice&password=secret123
Response (200 OK):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "bearer",
"expires_in": 3600,
"role": "admin",
"company_id": "company-uuid-123"
}Notes:
- JWT includes
jti(JWT ID) for tracking sessions inTokenSessiontable - Token expires after
ACCESS_TOKEN_EXPIRE_MINUTES(from config) - Tokens are validated by checking
TokenSessionrows at each protected endpoint - Login is rate-limited by IP + username and may return
429
Access: Authenticated (any user with valid JWT)
Revoke the current token session.
Request:
Authorization: Bearer <access_token>
Response (200 OK):
{
"message": "Successfully logged out"
}Notes:
- Sets
logout_atandrevoked_attimestamps on theTokenSessionrow - Subsequent requests with the same token will be rejected
Access: Authenticated (any user with valid JWT)
Retrieve the current user's profile.
Response (200 OK):
{
"id": "user-uuid-456",
"username": "alice",
"role": "admin",
"company_id": "company-uuid-123",
"company_name": "Acme Corp",
"created_at": "2024-01-15T10:30:00Z"
}Access: Authenticated (any user with valid JWT)
List all active token sessions for the current user.
Response (200 OK):
[
{
"id": "session-uuid-001",
"user_id": "user-uuid-456",
"jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"issued_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-01-15T11:30:00Z",
"revoked_at": null,
"logout_at": null,
"ip_address": null,
"user_agent": null
}
]Access: admin (own company) or super_admin (any company)
Create a new user (admin or employee) in a company.
Constraints:
- Cannot assign
super_adminrole (use bootstrap script for that) admincan only create users in their own companysuper_admincan create users in any company
Request:
{
"username": "bob",
"password": "newpassword456",
"role": "employee",
"company_id": "company-uuid-123"
}Response (201 Created):
{
"id": "user-uuid-789",
"username": "bob",
"role": "employee",
"company_id": "company-uuid-123",
"company_name": "Acme Corp",
"created_at": "2024-01-15T10:45:00Z"
}Errors:
400 Bad Request— username already registered or company not found403 Forbidden— insufficient role or cross-company attempt
Access: admin (own company) or super_admin (all companies)
List users.
Query Parameters:
company_id(optional, string UUID)- admin: ignored (always returns own company users)
- super_admin: filter by specified company; if omitted, returns all users (excluding other super_admins)
limit(optional, integer)- Page size. Uses
DEFAULT_LIST_LIMITwhen omitted. - Capped by
MAX_LIST_LIMIT.
- Page size. Uses
offset(optional, integer)- Zero-based row offset for pagination.
Response (200 OK):
[
{
"id": "user-uuid-456",
"username": "alice",
"role": "admin",
"company_id": "company-uuid-123",
"company_name": "Acme Corp",
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": "user-uuid-789",
"username": "bob",
"role": "employee",
"company_id": "company-uuid-123",
"company_name": "Acme Corp",
"created_at": "2024-01-15T10:45:00Z"
}
]Errors:
401 Unauthorized— invalid or missing JWT403 Forbidden— insufficient role
Access: admin (same company) or super_admin (any user)
Retrieve a specific user's profile.
Response (200 OK):
{
"id": "user-uuid-789",
"username": "bob",
"role": "employee",
"company_id": "company-uuid-123",
"company_name": "Acme Corp",
"created_at": "2024-01-15T10:45:00Z"
}Errors:
404 Not Found— user does not exist403 Forbidden— insufficient role or cross-company attempt
Access: admin (same company) or super_admin (any user)
Update a user's password, role, or company assignment.
Constraints:
- Cannot promote to
super_admin admincan only update users in their own company and cannot move them to a different companysuper_admincan reassign users to any company
Request:
{
"password": "updatedpassword789",
"role": "admin",
"company_id": "company-uuid-456"
}All fields are optional; only specified fields are updated.
Response (200 OK):
{
"id": "user-uuid-789",
"username": "bob",
"role": "admin",
"company_id": "company-uuid-456",
"company_name": "Acme Corp North",
"created_at": "2024-01-15T10:45:00Z"
}Errors:
404 Not Found— user does not exist403 Forbidden— insufficient role, cross-company attempt, or attempted super_admin promotion
Access: admin (same company) or super_admin (any user)
Delete a user account and cascade-delete all their token sessions.
Constraints:
admincan only delete users in their own companysuper_admincan delete any user
Response (204 No Content)
Errors:
404 Not Found— user does not exist403 Forbidden— insufficient role or cross-company attempt
Access: admin (their company) or super_admin (optional company filter)
List all token sessions within a company.
Query Parameters:
company_id(optional, string UUID)- admin: may be omitted or equal to their own company; any other value returns
403 - super_admin: optional filter; if omitted, returns sessions across all company users
- admin: may be omitted or equal to their own company; any other value returns
Response (200 OK):
[
{
"id": "session-uuid-001",
"user_id": "user-uuid-456",
"jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"issued_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-01-15T11:30:00Z",
"revoked_at": null,
"logout_at": null,
"ip_address": null,
"user_agent": null
}
]Access: super_admin only
Create a new company (tenant).
Request:
{
"name": "Acme Corp",
"industry": "Manufacturing",
"description": "Leading industrial solutions provider"
}Response (201 Created):
{
"id": "company-uuid-123",
"name": "Acme Corp",
"industry": "Manufacturing",
"description": "Leading industrial solutions provider",
"created_at": "2024-01-15T09:00:00Z"
}Errors:
400 Bad Request— company name already exists403 Forbidden— insufficient role
Access: super_admin only
List all companies.
Query Parameters:
limit(optional, integer)- Page size. Uses
DEFAULT_LIST_LIMITwhen omitted. - Capped by
MAX_LIST_LIMIT.
- Page size. Uses
offset(optional, integer)- Zero-based row offset for pagination.
Response (200 OK):
[
{
"id": "company-uuid-123",
"name": "Acme Corp",
"industry": "Manufacturing",
"description": "Leading industrial solutions provider",
"created_at": "2024-01-15T09:00:00Z"
},
{
"id": "company-uuid-456",
"name": "TechCorp",
"industry": "Software",
"description": "Cloud-first SaaS platform",
"created_at": "2024-01-15T09:15:00Z"
}
]Access: super_admin only
Retrieve a specific company's details.
Response (200 OK):
{
"id": "company-uuid-123",
"name": "Acme Corp",
"industry": "Manufacturing",
"description": "Leading industrial solutions provider",
"created_at": "2024-01-15T09:00:00Z"
}Errors:
404 Not Found— company does not exist
Access: super_admin only
Update company details.
Request:
{
"name": "Acme Corp Rebranded",
"industry": "Diversified Manufacturing",
"description": "Updated description"
}All fields are optional.
Response (200 OK):
{
"id": "company-uuid-123",
"name": "Acme Corp Rebranded",
"industry": "Diversified Manufacturing",
"description": "Updated description",
"created_at": "2024-01-15T09:00:00Z"
}Access: super_admin only
Delete a company. This cascades through related users, token sessions, documents, chunks, and chat history.
Response (204 No Content)
Notes:
- Deleting a company should be rare in production. Consider archiving instead.
Access: admin (own company) or super_admin (any company)
Upload a PDF for ingestion and vector embedding.
Query Parameters:
company_id(string UUID)- admin: ignored (always uploads to own company)
- super_admin: required and must reference an existing company
Request:
Content-Type: multipart/form-data
file=<binary PDF data>
Response (202 Accepted):
{
"message": "Document accepted for processing",
"document_id": "doc-uuid-001",
"status": "pending"
}Notes:
- The file is stored to S3 or local storage
- Celery worker processes asynchronously
- Status will transition:
pending→processing→completedorfailed - Successful upload invalidates related document and retrieval caches for the target company
- Uploads above
MAX_UPLOAD_BYTESare rejected with413
Errors:
400 Bad Request— company not found413 Payload Too Large— uploaded file exceeds configured upload limit403 Forbidden— insufficient role
Access: admin (own company) or super_admin (any company)
List documents.
This endpoint may serve short-lived Redis-cached metadata responses.
Query Parameters:
company_id(optional, string UUID)- admin: ignored (always returns own company documents)
- super_admin: filter by specified company; if omitted, returns all documents
limit(optional, integer)- Page size. Uses
DEFAULT_LIST_LIMITwhen omitted. - Capped by
MAX_LIST_LIMIT.
- Page size. Uses
offset(optional, integer)- Zero-based row offset for pagination.
Response (200 OK):
[
{
"id": "doc-uuid-001",
"filename": "acme_contracts_2024.pdf",
"status": "completed",
"created_at": "2024-01-15T10:15:00Z"
},
{
"id": "doc-uuid-002",
"filename": "employee_handbook.pdf",
"status": "processing",
"created_at": "2024-01-15T10:20:00Z"
}
]Access: admin (same company) or super_admin (any document)
Retrieve a specific document's status and metadata.
This endpoint may serve short-lived Redis-cached metadata responses.
Response (200 OK):
{
"id": "doc-uuid-001",
"filename": "acme_contracts_2024.pdf",
"status": "completed",
"created_at": "2024-01-15T10:15:00Z"
}Document Statuses:
PENDING— queued for processingPROCESSING— Celery worker is chunking/embeddingCOMPLETED— chunks stored indocument_chunkstableFAILED— error during processing
Errors:
404 Not Found— document does not exist403 Forbidden— insufficient role or cross-company attempt
Access: admin (same company) or super_admin (any document)
Delete a document and cascade-delete all its vector chunks from the database. Also removes the source PDF from storage. Related document/retrieval caches are invalidated.
Response (204 No Content)
Errors:
404 Not Found— document does not exist403 Forbidden— insufficient role or cross-company attempt
Access: admin and employee (company users only); super_admin blocked
Invoke the agent with a natural language query. Returns a grounded conversational response.
Safeguards:
- LangGraph recursion limit is capped at
8per request. - Repeated identical
search_documentsqueries are blocked after one repeat to prevent loops. - Requests are rate-limited per authenticated user.
- Optional idempotency: send
X-Idempotency-Keyto deduplicate retried invoke requests.
Request:
{
"query": "What are the key terms in our contracts?",
"conversation_id": "optional-existing-conversation-uuid"
}Response (200 OK):
{
"response": "Based on the analyzed contracts, the key terms include...",
"conversation_id": "conversation-uuid-123"
}Notes:
- Omit
conversation_idto create a new persisted conversation. - Provide
conversation_idto continue an existing conversation. - Conversation access is user-scoped and company-scoped.
- Successful writes invalidate chat history caches for that conversation/user.
Access: admin and employee (company users only); super_admin blocked
Stream the agent response as Server-Sent Events (SSE).
Safeguards:
- Same rate limiting and company scoping as
/v1/chat/invoke. - Optional idempotency via
X-Idempotency-Key. - Conversation ownership checks are identical to bulk invoke.
Request:
{
"query": "Summarize payment terms in our latest contracts",
"conversation_id": "optional-existing-conversation-uuid"
}Headers:
Authorization: Bearer <token>Content-Type: application/jsonAccept: text/event-stream
Response (200 OK):
Content-Type: text/event-stream- Stream of events until completion.
Event types:
token: incremental assistant text chunktool_start: tool invocation startedtool_end: tool invocation completederror: non-successful terminal conditiondone: successful terminal event with conversation metadata
Example stream payload:
event: token
data: {"text":"Based on your uploaded contracts,"}
event: token
data: {"text":" payment is due within 30 days."}
event: done
data: {"conversation_id":"conversation-uuid-123","cached":false}
Notes:
- The server persists one final assistant message when generation completes.
- If idempotency cache is hit, one
tokenevent may contain the full cached response followed bydonewithcached=true. - Treat
erroras terminal for that request.
cURL example:
curl -N -X POST "http://localhost:8000/v1/chat/stream" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"query":"Summarize the latest policy updates"}'JavaScript (Fetch + stream parser) example:
const res = await fetch("http://localhost:8000/v1/chat/stream", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ query: "Summarize the latest policy updates" }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n");
buffer = events.pop() ?? "";
for (const rawEvent of events) {
const eventLine = rawEvent.split("\n").find((line) => line.startsWith("event: "));
const dataLine = rawEvent.split("\n").find((line) => line.startsWith("data: "));
if (!eventLine || !dataLine) continue;
const eventName = eventLine.slice(7).trim();
const payload = JSON.parse(dataLine.slice(6));
if (eventName === "token") {
appendToUi(payload.text);
} else if (eventName === "done") {
saveConversationId(payload.conversation_id);
} else if (eventName === "error") {
showError(payload.detail);
}
}
}Access: admin and employee (company users only); super_admin blocked
List persisted conversations owned by the authenticated user.
This endpoint may serve short-lived Redis-cached responses.
Query Parameters:
limit(optional, integer)- Page size. Uses
DEFAULT_LIST_LIMITwhen omitted. - Capped by
MAX_LIST_LIMIT.
- Page size. Uses
offset(optional, integer)- Zero-based row offset for pagination.
Response (200 OK):
[
{
"id": "conversation-uuid-123",
"title": "What are the key terms in our contracts?",
"created_at": "27:06:2026 10:15:00.123",
"updated_at": "27:06:2026 10:17:42.100"
}
]Access: admin and employee (company users only); super_admin blocked
Return persisted messages for one conversation owned by the authenticated user.
This endpoint may serve short-lived Redis-cached responses.
Query Parameters:
limit(optional, integer)- Page size. Uses
DEFAULT_LIST_LIMITwhen omitted. - Capped by
MAX_LIST_LIMIT.
- Page size. Uses
offset(optional, integer)- Zero-based row offset for pagination.
Response (200 OK):
[
{
"id": "message-uuid-1",
"conversation_id": "conversation-uuid-123",
"query": "What are the key terms in our contracts?",
"response": "Based on the analyzed contracts, the key terms include...",
"created_at": "27:06:2026 10:15:02.512"
}
]Errors:
404 Not Found— conversation does not exist or does not belong to the authenticated user502 Bad Gateway— agent orchestration failed (LLM/tool failure surfaced asAgentError)500 Internal Server Error— unexpected unhandled server error
Process liveness check. Returns 200 when API process is up.
Dependency readiness check.
- Validates database connectivity.
- Validates Redis connectivity when
READINESS_REQUIRE_REDIS=true. - Returns
503when dependencies are not ready.
All endpoints follow standard HTTP status codes and return error details in JSON:
{
"detail": "User not found"
}| Status | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 202 | Accepted (async processing) |
| 204 | No Content (successful DELETE) |
| 400 | Bad Request (validation failed) |
| 401 | Unauthorized (invalid/missing JWT) |
| 403 | Forbidden (role/company insufficient) |
| 404 | Not Found (resource missing) |
| 413 | Payload Too Large (upload exceeds configured maximum) |
| 429 | Too Many Requests (rate-limited) |
| 502 | Bad Gateway (agent/tool orchestration failure) |
| 500 | Internal Server Error |
-
super_admin creates the company:
POST /v1/companies/ { "name": "New Corp", "industry": "...", "description": "..." } -
super_admin creates the first company admin:
POST /v1/users/ { "username": "admin@newcorp", "password": "...", "role": "admin", "company_id": "<company-uuid>" } -
admin@newcorp logs in:
POST /v1/auth/login username=admin@newcorp&password=... -
admin@newcorp creates employees and manages documents:
POST /v1/users/ { "username": "employee1@newcorp", "password": "...", "role": "employee", "company_id": "<company-uuid>" } POST /v1/documents/upload file=<PDF data> -
employee1@newcorp logs in and can access chat (query documents).
The super_admin role cannot be created or promoted via the API. Instead, use the
bootstrap script:
python scripts/create_superadmin.py \
--username superadmin \
--password <secure-password>This ensures super_admin creation is a controlled, out-of-band process.
Access tokens are signed JWTs with the following claims:
{
"sub": "username",
"role": "admin",
"company_id": "company-uuid-123",
"jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"iat": 1705339800,
"exp": 1705343400
}sub— username (unique identifier)role—super_admin,admin, oremployeecompany_id— company UUID (NULL for super_admin, set for others)jti— JWT ID, used to track and revoke sessionsiat— issued at (Unix timestamp)exp— expiration time (Unix timestamp)
Every protected endpoint validates the JWT signature, checks expiry, and verifies the
TokenSession row exists and hasn't been revoked/logged-out.