API Reference
Overview
All API endpoints are served by the Node.js Gateway at port :4000. The gateway handles JWT creation/validation, CORS, and request validation, then bridges every request to Rust Core via NATS messaging. The gateway never touches PostgreSQL directly.
Base URL: http://localhost:4000 (development)
Request flow:
Client -> Node.js Gateway (:4000) -> NATS -> Rust Core -> PostgreSQL
API Design Philosophy
Gateway-mediated: The HTTP layer is a thin Node.js service. All business logic and data access lives in Rust Core, reached via NATS request-reply. This separation keeps auth concerns isolated from the data layer.
Tenant-scoped: Authentication includes tenant context via JWT claims. You never specify tenant_id in request bodies -- it is derived from your access token.
Structured errors: Every error includes a machine-readable code, human-readable message, and optional details.
Endpoint Categories
| Category | Prefix | Auth Required | Description |
|---|---|---|---|
| Auth | /auth/* | Varies | Registration, login, token management, email verification |
| Users | /users/* | Yes | User profile, tenant list, avatar management |
| Teams | /teams/* | Yes | Team members, invitations, role management |
| Tenants | /tenants/* | Yes | Tenant settings and branding |
| Agents | /agents/* | Yes | Agent CRUD (create, list, update, delete) |
| Conversations | /conversations/* | Yes | Chat conversations, messages, streaming |
| Flows | /flows/* | Yes | Visual workflow definitions and execution |
| Artifact Templates | /artifact-templates/* | Yes | Structured output templates for agents |
| Knowledge | /knowledge/* | Yes | Document ingestion with human-in-the-loop gating |
| Files | /files/* | Yes | File upload, download, deletion (S3-backed) |
/email/* | Yes | Send templated emails | |
| Notifications | /notifications/* | Yes | Email templates and notification audit log |
| Audit | /audit/* | Yes | NATS bus audit trail |
| Admin | /admin/* | Super Admin | Platform-wide tenant/user management |
| Health | /health* | No | Liveness and readiness checks |
Authentication
JWT Authentication
Qadra uses short-lived access tokens and long-lived refresh tokens. The gateway creates and validates JWTs -- Rust Core is not involved in token mechanics.
Access token (15 min): Carries user identity and tenant context.
Refresh token (7 days): Used to obtain new access tokens. SHA256-hashed before database storage.
Use access token in requests:
Authorization: Bearer eyJ0eXAiOiJKV1Q...
JWT Claims
Access tokens contain these claims:
| Claim | Type | Description |
|---|---|---|
sub | string (UUID) | User ID |
email | string | User email |
name | string | User display name |
tid | string (UUID) | Active tenant ID |
tname | string | Active tenant name |
role | string | Per-tenant role (owner, admin, user, or super_admin for drop-in) |
sa | boolean? | Present and true only for super admins. Omitted for regular users. |
Refresh tokens contain only { sub, type: "refresh" }.
Auth Endpoints
Authentication and session management. Registration and login do not require auth. All others require a valid access token unless noted.
| Method | Path | Auth | NATS Subject | Description |
|---|---|---|---|---|
POST | /auth/register | No | qadra.auth.register | Create user + tenant, return tokens |
POST | /auth/login | No | qadra.auth.login | Verify credentials, return tokens |
POST | /auth/refresh | No | qadra.auth.refresh + store_token + revoke_token | Rotate token pair |
GET | /auth/verify-email?token=xxx | No | qadra.auth.verify_email | Verify email token |
POST | /auth/logout | Yes | qadra.auth.revoke_token | Revoke refresh token |
POST | /auth/switch-tenant | Yes | qadra.auth.get_user or qadra.admin.switch_tenant | Switch active tenant, get new access token |
POST | /auth/accept-invite | Yes | qadra.auth.accept_invite | Accept team invite by token |
POST | /auth/resend-verification | Yes | qadra.auth.resend_verification | Resend verification email |
Register
POST /auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword",
"name": "Jane Doe"
}
Response (201 Created):
{
"access_token": "eyJ0eXAiOiJKV1Q...",
"refresh_token": "eyJ0eXAiOiJKV1Q...",
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "Jane Doe",
"tenants": [
{
"tenant_id": "uuid",
"tenant_name": "Jane Doe's Workspace",
"role": "owner",
"is_default": true
}
]
},
"active_tenant": {
"tenant_id": "uuid",
"tenant_name": "Jane Doe's Workspace",
"role": "owner",
"is_default": true
}
}
Login
POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword"
}
Response (200 OK): Same shape as register response.
Refresh Tokens
POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJ0eXAiOiJKV1Q..."
}
Response (200 OK): Returns new access_token, refresh_token, user, and active_tenant. The old refresh token is revoked (rotation).
Switch Tenant
POST /auth/switch-tenant
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"tenant_id": "uuid"
}
Response (200 OK):
{
"access_token": "eyJ0eXAiOiJKV1Q...",
"active_tenant": {
"tenant_id": "uuid",
"tenant_name": "Acme Corp",
"role": "admin"
}
}
For super admins (sa: true in JWT), this bypasses membership checks and creates a synthetic JWT with role: "super_admin".
User Endpoints
All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /users/me | qadra.auth.get_user | Get current user profile + tenants |
GET | /users/me/tenants | qadra.auth.get_user | List user's tenant memberships |
POST | /users/me/avatar | qadra.auth.file.create + qadra.auth.update_avatar | Upload avatar (multipart, max 2MB, auto-resized to 256x256 WebP) |
DELETE | /users/me/avatar | qadra.auth.update_avatar | Remove avatar |
Get Profile
GET /users/me
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Response:
{
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "Jane Doe",
"avatar_url": "/api/files/uuid/download",
"email_verified": true,
"tenants": [...]
},
"active_tenant": {
"tenant_id": "uuid",
"tenant_name": "Acme Corp",
"role": "admin"
}
}
Upload Avatar
POST /users/me/avatar
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: multipart/form-data
file: <image file (JPEG, PNG, or WebP, max 2MB)>
The image is automatically resized to 256x256 and converted to WebP via the imaginary service.
Team Endpoints
Manage team members and invitations for the current tenant. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /teams/members | qadra.auth.list_members | List tenant members |
POST | /teams/members/invite | qadra.auth.invite_member | Invite by email (smart: add directly or send invite) |
PATCH | /teams/members/:userId/role | qadra.auth.update_member_role | Change member role (owner/admin only) |
DELETE | /teams/members/:userId | qadra.auth.remove_member | Remove member (owner/admin only) |
GET | /teams/invites | qadra.auth.list_invites | List pending invites |
DELETE | /teams/invites/:inviteId | qadra.auth.cancel_invite | Cancel pending invite (owner/admin only) |
Invite Member
POST /teams/members/invite
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"email": "colleague@example.com",
"role": "admin"
}
Smart invite logic: If a user with that email already exists on the platform, they are added to the tenant directly. If not, an invitation email is sent.
Response (201 Created):
{
"success": true,
"added_directly": false,
"message": "Invitation email sent"
}
Tenant Endpoints
Manage the current tenant's settings and branding. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /tenants/current | qadra.auth.get_tenant | Get current tenant settings + branding |
PATCH | /tenants/current | qadra.auth.update_tenant | Update tenant name/settings (owner/admin only) |
Update Tenant
PATCH /tenants/current
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"name": "Acme Corp",
"settings": {
"branding": { "primary_color": "#1a73e8" },
"storage": { "provider": "internal" }
}
}
Agent Endpoints
Manage tenant-scoped AI agents. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /agents | qadra.{tid}.agent.do.create | Create a new agent |
GET | /agents | qadra.{tid}.agent.do.list | List all agents (paginated) |
GET | /agents/:agentId | qadra.{tid}.agent.do.get | Get agent by ID |
PUT | /agents/:agentId | qadra.{tid}.agent.do.update | Update agent |
DELETE | /agents/:agentId | qadra.{tid}.agent.do.delete | Delete agent |
Create Agent
POST /agents
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"name": "research-analyst",
"display_name": "Research Analyst",
"description": "Gathers market data and competitive analysis",
"system_prompt": "You are a research analyst...",
"control_type": "relinquish_to_parent",
"output_visibility": "user_facing",
"max_calls_per_parent_agent": 3
}
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique agent identifier |
display_name | string? | null | Human-readable name |
avatar_url | string? | null | Agent avatar URL |
description | string? | null | What this agent does |
system_prompt | string | "" | System prompt for LLM context |
control_type | enum | relinquish_to_parent | retain, relinquish_to_parent, or relinquish_to_start |
output_visibility | enum | user_facing | user_facing or internal |
max_calls_per_parent_agent | int | 3 | Safety rail: max invocations per parent |
Conversation Endpoints
Chat-style conversations with agents. Supports both synchronous and SSE-streaming message delivery. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /conversations | qadra.{tid}.chat.do.create | Create a conversation |
GET | /conversations | qadra.{tid}.chat.do.list | List conversations (paginated) |
GET | /conversations/:id | qadra.{tid}.chat.do.get | Get conversation by ID |
PATCH | /conversations/:id | qadra.{tid}.chat.do.update_title | Update conversation title |
DELETE | /conversations/:id | qadra.{tid}.chat.do.delete | Soft-delete conversation |
POST | /conversations/:id/messages | qadra.{tid}.chat.do.send_message | Send message (synchronous, 240s timeout) |
POST | /conversations/:id/messages/stream | qadra.{tid}.chat.do.send_message | Send message with SSE progress stream |
GET | /conversations/:id/messages | qadra.{tid}.chat.do.list_messages | List messages (paginated) |
POST | /conversations/:id/messages/:messageId/rate | qadra.{tid}.chat.do.rate_message | Rate a message (+1 or -1) |
Send Message
POST /conversations/:id/messages
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"agent_id": "uuid",
"content": "Analyze the competitive landscape for Acme Corp",
"flow_execution_id": "uuid (optional)"
}
Stream Message (SSE)
POST /conversations/:id/messages/stream
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"agent_id": "uuid",
"content": "Analyze the competitive landscape for Acme Corp"
}
Returns a text/event-stream with progress events:
event: flow_started
data: {"execution_id":"uuid","flow_name":"research-flow"}
event: node_started
data: {"node_id":"agent-1","node_type":"Agent","label":"Web Search"}
event: node_completed
data: {"node_id":"agent-1","output":{...}}
event: assistant_message
data: {"id":"uuid","role":"assistant","content":"Based on my analysis..."}
event: done
data: {}
Flow Endpoints
Visual graph-based workflows (ReactFlow). Create flow definitions, execute them, and manage executions with human-in-the-loop gates. All require authentication.
Flow Definitions
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /flows | qadra.{tid}.flow.do.create | Create a new flow |
GET | /flows | qadra.{tid}.flow.do.list | List all flows (paginated) |
GET | /flows/:flowId | qadra.{tid}.flow.do.get | Get flow by ID |
PUT | /flows/:flowId | qadra.{tid}.flow.do.update | Update flow (name, description, flow_data, is_active) |
DELETE | /flows/:flowId | qadra.{tid}.flow.do.delete | Delete flow |
POST | /flows/generate | qadra.{tid}.flow.do.generate | AI-generate a flow from natural language prompt |
Flow Executions
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /flows/:flowId/execute | qadra.{tid}.flow.do.execute | Start flow execution |
GET | /flows/:flowId/executions | qadra.{tid}.flow.do.execution_list | List executions for a flow |
GET | /flows/executions/:execId | qadra.{tid}.flow.do.execution_get | Get execution status and state |
POST | /flows/executions/:execId/cancel | qadra.{tid}.flow.do.execution_cancel | Cancel running execution |
POST | /flows/executions/:execId/input | qadra.{tid}.flow.do.human_input | Submit human input for paused execution |
Create Flow
POST /flows
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"name": "research-pipeline",
"description": "Multi-step research workflow",
"flow_data": {
"nodes": [...],
"edges": [...],
"viewport": { "x": 0, "y": 0, "zoom": 1 }
}
}
Execute Flow
POST /flows/:flowId/execute
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"input": {
"target_company": "Acme Corp",
"analysis_type": "competitive"
}
}
Submit Human Input
When an execution is paused at a HumanInput node:
POST /flows/executions/:execId/input
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"node_id": "human-input-1",
"response": {
"approved": true,
"notes": "Proceed with analysis"
}
}
AI Flow Generation
Generate a flow graph from a natural language description:
POST /flows/generate
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"prompt": "Create a workflow that researches a company, validates findings, and produces a summary report"
}
Flow Node Types
| Type | Description |
|---|---|
Start | Entry point, seeds initial state |
End | Terminal node, marks execution complete |
Agent | Invokes an AI agent with context |
Condition | Evaluates expression, branches flow |
HumanInput | Pauses execution, waits for user response |
Loop | Iterates over collection (max 100 iterations) |
Http | Makes external HTTP request |
ExecuteFlow | Runs a sub-flow |
Transform | JavaScript expression to transform state |
Delay | Pauses execution for a duration (max 300s) |
SpoFilter | Queries SPO triples into state, with an optional asserter filter |
SpoWrite | Persists agent output as SPO triples with provenance (config: content_key, asserter, source, output_key) |
ClaimValidation | Grounds prior-stage claims against SPO triples (config: input_keys default scarlet_output,ayana_output, output_key, asserter_filter, query_limit); reports per-claim SUPPORTED/CONTRADICTED/NOT_MENTIONED |
Execution Status Lifecycle
pending -> running -> completed
-> failed
-> cancelled
running -> paused (HumanInput) -> running (after input submitted)
Artifact Template Endpoints
Define structured output formats for agent-produced artifacts. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /artifact-templates | qadra.{tid}.artifact_template.do.create | Create a template |
GET | /artifact-templates | qadra.{tid}.artifact_template.do.list | List templates (paginated, filterable by artifact_type) |
GET | /artifact-templates/:id | qadra.{tid}.artifact_template.do.get | Get template by ID |
PUT | /artifact-templates/:id | qadra.{tid}.artifact_template.do.update | Update template |
DELETE | /artifact-templates/:id | qadra.{tid}.artifact_template.do.delete | Delete template |
Create Artifact Template
POST /artifact-templates
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"name": "investment-memo",
"display_name": "Investment Memo",
"description": "Structured investment memorandum",
"artifact_type": "document",
"expected_sections": [
{ "name": "Executive Summary", "required": true },
{ "name": "Market Analysis", "required": true },
{ "name": "Risk Factors", "required": true },
{ "name": "Recommendation", "required": true }
],
"output_format": "markdown"
}
Knowledge Endpoints
Human-gated document ingestion into the knowledge graph. Agents or users propose documents; humans approve or reject before ingestion runs. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /knowledge/propose | qadra.{tid}.epistemic.propose_document | Propose a document for review |
POST | /knowledge/documents/:id/approve | qadra.{tid}.epistemic.approve_document | Approve a pending document (triggers async ingestion) |
POST | /knowledge/documents/:id/reject | qadra.{tid}.epistemic.reject_document | Reject a pending document |
POST | /knowledge/ingest | qadra.{tid}.epistemic.ingest_document | Direct ingestion (bypass proposal gate, 2 min timeout) |
GET | /knowledge/documents | qadra.{tid}.epistemic.list_documents | List ingested documents (paginated, filterable by status) |
GET | /knowledge/documents/:id | qadra.{tid}.epistemic.get_document | Get single document |
Propose Document
POST /knowledge/propose
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"title": "Company Overview",
"content": "Apple was founded by Steve Jobs in 1976...",
"content_type": "text/plain",
"source_url": "https://example.com/article",
"source_type": "web"
}
Response (201 Created):
{
"document_id": "uuid"
}
Approve Document
POST /knowledge/documents/:id/approve
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Approval returns immediately. Ingestion runs asynchronously on the Rust side. Poll GET /knowledge/documents/:id for status updates.
Response:
{
"document_id": "uuid",
"status": "approved",
"message": "Ingestion started -- poll document status for progress"
}
Direct Ingestion
POST /knowledge/ingest
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"title": "Company Overview",
"content": "Apple was founded by Steve Jobs in 1976...",
"content_type": "text/plain",
"source_url": "https://example.com/article",
"source_type": "upload"
}
Response (201 Created):
{
"document_id": "uuid",
"routing": "both",
"triples_extracted": 5,
"chunks_created": 3,
"duration_ms": 2340
}
Max content length: 1MB of text per document.
Ingest Triples
Ingest pre-extracted SPO triples directly, bypassing document analysis and triple extraction. Useful when triples are produced upstream (e.g., by a SpoWrite flow node). Each triple carries its provenance.
POST /knowledge/ingest-triples
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"triples": [
{
"subject": "Apple",
"predicate": "founded_by",
"object": "Steve Jobs",
"confidence": 0.95,
"source": "Company Overview",
"asserter": "research-analyst",
"assertion_type": "fact"
}
]
}
NATS subject: qadra.{tid}.epistemic.ingest.
Trace Claim
Trace a memo claim back to the supporting SPO triples (each with its asserter and source) and the source documents (with excerpt and content) that ground it. The server enforces a 450 ms internal budget to meet a sub-500 ms acceptance criterion.
POST /knowledge/trace
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"claim": "Apple was founded by Steve Jobs in 1976",
"max_results": 5
}
| Field | Type | Default | Description |
|---|---|---|---|
claim | string | required | Claim text to trace (1-2000 chars) |
max_results | int? | null | Max matches to return (1-20) |
NATS subject: qadra.{tid}.epistemic.trace_claim.
Response:
{
"claim": "Apple was founded by Steve Jobs in 1976",
"matches": [
{
"triple": {
"subject": "Apple",
"predicate": "founded_by",
"object": "Steve Jobs",
"confidence": 0.95,
"source": "Company Overview",
"asserter": "research-analyst",
"assertion_type": "fact"
},
"documents": [
{
"document_id": "uuid",
"title": "Company Overview",
"source_url": "https://example.com/article",
"source_type": "web",
"content": "Apple was founded by Steve Jobs in 1976...",
"excerpt": "Apple was founded by Steve Jobs in 1976",
"confidence": 0.95
}
]
}
],
"duration_ms": 312
}
Knowledge Conflicts
Detect and triage competing assertions on the same subject/predicate. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /knowledge/conflicts | qadra.{tid}.epistemic.list_conflicts | List detected conflicts (paginated, filterable by subject) |
POST | /knowledge/conflicts/:id/acknowledge | qadra.{tid}.epistemic.acknowledge_conflict | Mark a conflict as seen |
POST | /knowledge/conflicts/:id/resolve | qadra.{tid}.epistemic.resolve_conflict | Resolve a conflict (records chosen assertion + reviewer) |
POST | /knowledge/conflicts/:id/dismiss | qadra.{tid}.epistemic.dismiss_conflict | Dismiss a conflict as not actionable |
List Conflicts
GET /knowledge/conflicts?subject=Apple&limit=25&offset=0
Authorization: Bearer eyJ0eXAiOiJKV1Q...
| Parameter | Type | Default | Description |
|---|---|---|---|
subject | string? | null | Filter by subject |
limit | int | 25 | Results per page (1-100) |
offset | int | 0 | Pagination offset |
Resolve Conflict
POST /knowledge/conflicts/:id/resolve
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"chosen_assertion": "Apple was founded in 1976",
"reason": "Confirmed against primary source filing"
}
Attribution Sessions
KG-SMILE gate-run audit (Story 104). Every call into the epistemic query pipeline persists a gate run with its per-triple attributions. All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /knowledge/attribution-sessions | qadra.{tid}.epistemic.list_attribution_sessions | List KG-SMILE gate runs (paginated) |
GET | /knowledge/attribution-sessions/:id | qadra.{tid}.epistemic.get_attribution_session | Get one gate run with its per-triple attributions |
List Attribution Sessions
GET /knowledge/attribution-sessions?workload_id=uuid&competence_passed=true&limit=25&offset=0
Authorization: Bearer eyJ0eXAiOiJKV1Q...
| Parameter | Type | Default | Description |
|---|---|---|---|
workload_id | string (UUID)? | null | Filter by workload |
agent_name | string? | null | Filter by agent name |
from | string (ISO 8601)? | null | Start of date range |
to | string (ISO 8601)? | null | End of date range |
competence_passed | boolean? | null | Filter by competence gate result |
verification_passed | boolean? | null | Filter by verification gate result |
limit | int | 25 | Results per page (1-100) |
offset | int | 0 | Pagination offset |
File Endpoints
Generic file upload and download backed by S3-compatible storage (MinIO by default, tenant-configurable). All require authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /files/upload | qadra.auth.file.create | Upload file (multipart, purpose-based validation) |
GET | /files/:fileId/download | qadra.auth.file.get | Download file (proxied from S3) |
DELETE | /files/:fileId | qadra.auth.file.get + qadra.auth.file.delete | Delete file (S3 + metadata) |
Upload File
POST /files/upload
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: multipart/form-data
file: <file data>
purpose: "document"
entity_type: "workload" (optional)
entity_id: "uuid" (optional)
Purpose-based validation:
| Purpose | Max Size | Allowed Types |
|---|---|---|
avatar | 2 MB | image/jpeg, image/png, image/webp |
logo | 2 MB | image/jpeg, image/png, image/webp, image/svg+xml |
| Other | 10 MB | Any |
Download File
GET /files/:fileId/download
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Returns the file content with appropriate Content-Type and Content-Disposition headers. Downloads are always proxied through the gateway -- never direct S3 links.
Email Endpoints
Send templated emails. Requires authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
POST | /email/send | qadra.email.send | Send templated email |
Send Email
POST /email/send
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"to": ["recipient@example.com"],
"template_slug": "welcome",
"variables": {
"user_name": "Jane",
"tenant_name": "Acme Corp"
}
}
Response:
{
"success": true,
"email_id": "smtp2go-id",
"succeeded": 1,
"failed": 0
}
Notification Endpoints
Manage email templates and view the notification audit log. All require authentication. Template mutations and log access require owner/admin role.
Email Templates
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /notifications/templates | qadra.email.template.list | List all email templates (global + tenant) |
GET | /notifications/templates/:slug | qadra.email.template.get | Get template by slug |
PUT | /notifications/templates/:slug | qadra.email.template.upsert | Create/update tenant template override (owner/admin) |
DELETE | /notifications/templates/:slug | qadra.email.template.delete | Delete tenant template override (owner/admin) |
POST | /notifications/templates/:slug/test | qadra.email.template.test_send | Send test email to self (owner/admin) |
Notification Log
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /notifications/logs | qadra.email.logs | List notification audit log (owner/admin, paginated) |
Upsert Template
PUT /notifications/templates/welcome
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Content-Type: application/json
{
"name": "Welcome Email",
"subject": "Welcome to {{tenant_name}}, {{user_name}}!",
"html_body": "<h1>Welcome!</h1><p>Hello {{user_name}}...</p>",
"text_body": "Welcome! Hello {{user_name}}..."
}
Templates use Handlebars {{var}} syntax for variable substitution.
Audit Endpoints
Query the NATS bus audit trail stored in MongoDB. Requires authentication.
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /audit/logs | qadra.audit.list | List NATS audit trail (paginated, filterable) |
List Audit Logs
GET /audit/logs?category=auth&operation=login&limit=25&offset=0
Authorization: Bearer eyJ0eXAiOiJKV1Q...
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
category | string? | null | Filter by category (e.g., auth, flow, epistemic) |
operation | string? | null | Filter by operation (e.g., login, create, execute) |
limit | int | 25 | Results per page (1-100) |
offset | int | 0 | Pagination offset |
Response:
{
"logs": [
{
"entry_id": "uuid",
"timestamp": "2026-03-18T10:00:00Z",
"subject": "qadra.auth.login",
"category": "auth",
"operation": "login",
"payload_bytes": 142
}
],
"pagination": {
"total": 500,
"limit": 25,
"offset": 0,
"has_more": true
}
}
Super Admin Endpoints
Platform-wide administrative operations. All require requireAuth + requireSuperAdmin middleware (JWT must have sa: true).
| Method | Path | NATS Subject | Description |
|---|---|---|---|
GET | /admin/tenants | qadra.admin.list_tenants | List all tenants (paginated) |
POST | /admin/tenants | qadra.admin.create_tenant | Create tenant with owner |
GET | /admin/users | qadra.admin.list_users | List all users (paginated) |
POST | /admin/users/:userId/reset-password | qadra.admin.reset_password | Reset user password |
POST | /admin/users/:userId/promote | qadra.admin.promote | Promote to super admin |
POST | /admin/users/:userId/demote | qadra.admin.demote | Remove super admin |
GET | /admin/stats | qadra.admin.stats | Platform stats (counts + recent entries) |
Create Tenant
POST /admin/tenants
Authorization: Bearer eyJ0eXAiOiJKV1Q... (super admin)
Content-Type: application/json
{
"name": "New Organization",
"owner_user_id": "uuid"
}
Platform Stats
GET /admin/stats
Authorization: Bearer eyJ0eXAiOiJKV1Q... (super admin)
Response:
{
"total_tenants": 42,
"total_users": 156,
"super_admin_count": 2,
"recent_tenants": [...],
"recent_users": [...]
}
Health Endpoints
No authentication required.
| Method | Path | Description |
|---|---|---|
GET | /health | Liveness check (always returns ok) |
GET | /health/ready | Readiness check (verifies NATS connectivity) |
Error Responses
All errors follow this format:
{
"error": {
"code": "WORKLOAD_NOT_FOUND",
"message": "Workload with ID xyz not found"
}
}
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or missing authentication |
FORBIDDEN | 403 | No access to resource (insufficient role) |
NOT_FOUND | 404 | Resource not found |
CONFLICT | 409 | Resource already exists (e.g., duplicate invite) |
VALIDATION_ERROR | 422 | Invalid request body |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Server error |
SERVICE_UNAVAILABLE | 502/503 | Backend (Rust Core via NATS) unreachable |
Pagination
Paginated endpoints accept limit and offset query parameters:
GET /flows?limit=20&offset=0
Limits are clamped to 1-100. Default limit is 20, default offset is 0.
Paginated responses include a pagination object:
{
"data": [...],
"pagination": {
"total": 150,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Not all paginated endpoints return a total count. List endpoints that return lightweight arrays (e.g., /teams/members, /flows) may omit the pagination wrapper and return { data: [...] } or { members: [...] } directly.