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

CategoryPrefixAuth RequiredDescription
Auth/auth/*VariesRegistration, login, token management, email verification
Users/users/*YesUser profile, tenant list, avatar management
Teams/teams/*YesTeam members, invitations, role management
Tenants/tenants/*YesTenant settings and branding
Agents/agents/*YesAgent CRUD (create, list, update, delete)
Conversations/conversations/*YesChat conversations, messages, streaming
Flows/flows/*YesVisual workflow definitions and execution
Artifact Templates/artifact-templates/*YesStructured output templates for agents
Knowledge/knowledge/*YesDocument ingestion with human-in-the-loop gating
Files/files/*YesFile upload, download, deletion (S3-backed)
Email/email/*YesSend templated emails
Notifications/notifications/*YesEmail templates and notification audit log
Audit/audit/*YesNATS bus audit trail
Admin/admin/*Super AdminPlatform-wide tenant/user management
Health/health*NoLiveness 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:

ClaimTypeDescription
substring (UUID)User ID
emailstringUser email
namestringUser display name
tidstring (UUID)Active tenant ID
tnamestringActive tenant name
rolestringPer-tenant role (owner, admin, user, or super_admin for drop-in)
saboolean?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.

MethodPathAuthNATS SubjectDescription
POST/auth/registerNoqadra.auth.registerCreate user + tenant, return tokens
POST/auth/loginNoqadra.auth.loginVerify credentials, return tokens
POST/auth/refreshNoqadra.auth.refresh + store_token + revoke_tokenRotate token pair
GET/auth/verify-email?token=xxxNoqadra.auth.verify_emailVerify email token
POST/auth/logoutYesqadra.auth.revoke_tokenRevoke refresh token
POST/auth/switch-tenantYesqadra.auth.get_user or qadra.admin.switch_tenantSwitch active tenant, get new access token
POST/auth/accept-inviteYesqadra.auth.accept_inviteAccept team invite by token
POST/auth/resend-verificationYesqadra.auth.resend_verificationResend 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.

MethodPathNATS SubjectDescription
GET/users/meqadra.auth.get_userGet current user profile + tenants
GET/users/me/tenantsqadra.auth.get_userList user's tenant memberships
POST/users/me/avatarqadra.auth.file.create + qadra.auth.update_avatarUpload avatar (multipart, max 2MB, auto-resized to 256x256 WebP)
DELETE/users/me/avatarqadra.auth.update_avatarRemove 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.

MethodPathNATS SubjectDescription
GET/teams/membersqadra.auth.list_membersList tenant members
POST/teams/members/inviteqadra.auth.invite_memberInvite by email (smart: add directly or send invite)
PATCH/teams/members/:userId/roleqadra.auth.update_member_roleChange member role (owner/admin only)
DELETE/teams/members/:userIdqadra.auth.remove_memberRemove member (owner/admin only)
GET/teams/invitesqadra.auth.list_invitesList pending invites
DELETE/teams/invites/:inviteIdqadra.auth.cancel_inviteCancel 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.

MethodPathNATS SubjectDescription
GET/tenants/currentqadra.auth.get_tenantGet current tenant settings + branding
PATCH/tenants/currentqadra.auth.update_tenantUpdate 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.

MethodPathNATS SubjectDescription
POST/agentsqadra.{tid}.agent.do.createCreate a new agent
GET/agentsqadra.{tid}.agent.do.listList all agents (paginated)
GET/agents/:agentIdqadra.{tid}.agent.do.getGet agent by ID
PUT/agents/:agentIdqadra.{tid}.agent.do.updateUpdate agent
DELETE/agents/:agentIdqadra.{tid}.agent.do.deleteDelete 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
}
FieldTypeDefaultDescription
namestringrequiredUnique agent identifier
display_namestring?nullHuman-readable name
avatar_urlstring?nullAgent avatar URL
descriptionstring?nullWhat this agent does
system_promptstring""System prompt for LLM context
control_typeenumrelinquish_to_parentretain, relinquish_to_parent, or relinquish_to_start
output_visibilityenumuser_facinguser_facing or internal
max_calls_per_parent_agentint3Safety rail: max invocations per parent

Conversation Endpoints

Chat-style conversations with agents. Supports both synchronous and SSE-streaming message delivery. All require authentication.

MethodPathNATS SubjectDescription
POST/conversationsqadra.{tid}.chat.do.createCreate a conversation
GET/conversationsqadra.{tid}.chat.do.listList conversations (paginated)
GET/conversations/:idqadra.{tid}.chat.do.getGet conversation by ID
PATCH/conversations/:idqadra.{tid}.chat.do.update_titleUpdate conversation title
DELETE/conversations/:idqadra.{tid}.chat.do.deleteSoft-delete conversation
POST/conversations/:id/messagesqadra.{tid}.chat.do.send_messageSend message (synchronous, 240s timeout)
POST/conversations/:id/messages/streamqadra.{tid}.chat.do.send_messageSend message with SSE progress stream
GET/conversations/:id/messagesqadra.{tid}.chat.do.list_messagesList messages (paginated)
POST/conversations/:id/messages/:messageId/rateqadra.{tid}.chat.do.rate_messageRate 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

MethodPathNATS SubjectDescription
POST/flowsqadra.{tid}.flow.do.createCreate a new flow
GET/flowsqadra.{tid}.flow.do.listList all flows (paginated)
GET/flows/:flowIdqadra.{tid}.flow.do.getGet flow by ID
PUT/flows/:flowIdqadra.{tid}.flow.do.updateUpdate flow (name, description, flow_data, is_active)
DELETE/flows/:flowIdqadra.{tid}.flow.do.deleteDelete flow
POST/flows/generateqadra.{tid}.flow.do.generateAI-generate a flow from natural language prompt

Flow Executions

MethodPathNATS SubjectDescription
POST/flows/:flowId/executeqadra.{tid}.flow.do.executeStart flow execution
GET/flows/:flowId/executionsqadra.{tid}.flow.do.execution_listList executions for a flow
GET/flows/executions/:execIdqadra.{tid}.flow.do.execution_getGet execution status and state
POST/flows/executions/:execId/cancelqadra.{tid}.flow.do.execution_cancelCancel running execution
POST/flows/executions/:execId/inputqadra.{tid}.flow.do.human_inputSubmit 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

TypeDescription
StartEntry point, seeds initial state
EndTerminal node, marks execution complete
AgentInvokes an AI agent with context
ConditionEvaluates expression, branches flow
HumanInputPauses execution, waits for user response
LoopIterates over collection (max 100 iterations)
HttpMakes external HTTP request
ExecuteFlowRuns a sub-flow
TransformJavaScript expression to transform state
DelayPauses execution for a duration (max 300s)
SpoFilterQueries SPO triples into state, with an optional asserter filter
SpoWritePersists agent output as SPO triples with provenance (config: content_key, asserter, source, output_key)
ClaimValidationGrounds 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.

MethodPathNATS SubjectDescription
POST/artifact-templatesqadra.{tid}.artifact_template.do.createCreate a template
GET/artifact-templatesqadra.{tid}.artifact_template.do.listList templates (paginated, filterable by artifact_type)
GET/artifact-templates/:idqadra.{tid}.artifact_template.do.getGet template by ID
PUT/artifact-templates/:idqadra.{tid}.artifact_template.do.updateUpdate template
DELETE/artifact-templates/:idqadra.{tid}.artifact_template.do.deleteDelete 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.

MethodPathNATS SubjectDescription
POST/knowledge/proposeqadra.{tid}.epistemic.propose_documentPropose a document for review
POST/knowledge/documents/:id/approveqadra.{tid}.epistemic.approve_documentApprove a pending document (triggers async ingestion)
POST/knowledge/documents/:id/rejectqadra.{tid}.epistemic.reject_documentReject a pending document
POST/knowledge/ingestqadra.{tid}.epistemic.ingest_documentDirect ingestion (bypass proposal gate, 2 min timeout)
GET/knowledge/documentsqadra.{tid}.epistemic.list_documentsList ingested documents (paginated, filterable by status)
GET/knowledge/documents/:idqadra.{tid}.epistemic.get_documentGet 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
}
FieldTypeDefaultDescription
claimstringrequiredClaim text to trace (1-2000 chars)
max_resultsint?nullMax 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.

MethodPathNATS SubjectDescription
GET/knowledge/conflictsqadra.{tid}.epistemic.list_conflictsList detected conflicts (paginated, filterable by subject)
POST/knowledge/conflicts/:id/acknowledgeqadra.{tid}.epistemic.acknowledge_conflictMark a conflict as seen
POST/knowledge/conflicts/:id/resolveqadra.{tid}.epistemic.resolve_conflictResolve a conflict (records chosen assertion + reviewer)
POST/knowledge/conflicts/:id/dismissqadra.{tid}.epistemic.dismiss_conflictDismiss a conflict as not actionable

List Conflicts

GET /knowledge/conflicts?subject=Apple&limit=25&offset=0
Authorization: Bearer eyJ0eXAiOiJKV1Q...
ParameterTypeDefaultDescription
subjectstring?nullFilter by subject
limitint25Results per page (1-100)
offsetint0Pagination 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.

MethodPathNATS SubjectDescription
GET/knowledge/attribution-sessionsqadra.{tid}.epistemic.list_attribution_sessionsList KG-SMILE gate runs (paginated)
GET/knowledge/attribution-sessions/:idqadra.{tid}.epistemic.get_attribution_sessionGet 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...
ParameterTypeDefaultDescription
workload_idstring (UUID)?nullFilter by workload
agent_namestring?nullFilter by agent name
fromstring (ISO 8601)?nullStart of date range
tostring (ISO 8601)?nullEnd of date range
competence_passedboolean?nullFilter by competence gate result
verification_passedboolean?nullFilter by verification gate result
limitint25Results per page (1-100)
offsetint0Pagination offset

File Endpoints

Generic file upload and download backed by S3-compatible storage (MinIO by default, tenant-configurable). All require authentication.

MethodPathNATS SubjectDescription
POST/files/uploadqadra.auth.file.createUpload file (multipart, purpose-based validation)
GET/files/:fileId/downloadqadra.auth.file.getDownload file (proxied from S3)
DELETE/files/:fileIdqadra.auth.file.get + qadra.auth.file.deleteDelete 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:

PurposeMax SizeAllowed Types
avatar2 MBimage/jpeg, image/png, image/webp
logo2 MBimage/jpeg, image/png, image/webp, image/svg+xml
Other10 MBAny

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.

MethodPathNATS SubjectDescription
POST/email/sendqadra.email.sendSend 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

MethodPathNATS SubjectDescription
GET/notifications/templatesqadra.email.template.listList all email templates (global + tenant)
GET/notifications/templates/:slugqadra.email.template.getGet template by slug
PUT/notifications/templates/:slugqadra.email.template.upsertCreate/update tenant template override (owner/admin)
DELETE/notifications/templates/:slugqadra.email.template.deleteDelete tenant template override (owner/admin)
POST/notifications/templates/:slug/testqadra.email.template.test_sendSend test email to self (owner/admin)

Notification Log

MethodPathNATS SubjectDescription
GET/notifications/logsqadra.email.logsList 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.

MethodPathNATS SubjectDescription
GET/audit/logsqadra.audit.listList NATS audit trail (paginated, filterable)

List Audit Logs

GET /audit/logs?category=auth&operation=login&limit=25&offset=0
Authorization: Bearer eyJ0eXAiOiJKV1Q...

Query parameters:

ParameterTypeDefaultDescription
categorystring?nullFilter by category (e.g., auth, flow, epistemic)
operationstring?nullFilter by operation (e.g., login, create, execute)
limitint25Results per page (1-100)
offsetint0Pagination 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).

MethodPathNATS SubjectDescription
GET/admin/tenantsqadra.admin.list_tenantsList all tenants (paginated)
POST/admin/tenantsqadra.admin.create_tenantCreate tenant with owner
GET/admin/usersqadra.admin.list_usersList all users (paginated)
POST/admin/users/:userId/reset-passwordqadra.admin.reset_passwordReset user password
POST/admin/users/:userId/promoteqadra.admin.promotePromote to super admin
POST/admin/users/:userId/demoteqadra.admin.demoteRemove super admin
GET/admin/statsqadra.admin.statsPlatform 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.

MethodPathDescription
GET/healthLiveness check (always returns ok)
GET/health/readyReadiness 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

CodeHTTP StatusDescription
UNAUTHORIZED401Invalid or missing authentication
FORBIDDEN403No access to resource (insufficient role)
NOT_FOUND404Resource not found
CONFLICT409Resource already exists (e.g., duplicate invite)
VALIDATION_ERROR422Invalid request body
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Server error
SERVICE_UNAVAILABLE502/503Backend (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.