AuditTrail API Reference

Base URL: /api/v1

All request and response bodies are JSON.

Authentication: All trace, analytics, evaluation, ablation, and span endpoints require a valid session cookie (set by POST /api/v1/auth/login). Unauthenticated requests return 401 Unauthorized. Ingest endpoints (POST /ingest/spans, POST /ingest/traces) use optional auth — authenticated users get traces assigned to their account; unauthenticated agents create unowned traces.

User isolation: Each user only sees traces where user_id matches their account. Traces created without authentication have user_id=NULL and are not visible to any user.

Response envelope: Most GET endpoints wrap their response in a {"data": ...} envelope. Always read resp.json()["data"] rather than resp.json() directly when consuming trace, span, DAG, or evaluation responses.


Health

GET /api/v1/health

Check service health, database connectivity, and uptime.

Response:

{
  "status": "ok",
  "version": "1.0.0",
  "db_connected": true,
  "rules_loaded": 6,
  "uptime_seconds": 3600.5
}
FieldTypeDescription
statusstring"ok" or "degraded"
versionstringAPI version
db_connectedbooleanWhether the database is reachable
rules_loadedintegerNumber of enabled constitutional rules
uptime_secondsfloatSeconds since server start

Auth

POST /api/v1/auth/register

Create a new user account. The first registered user is assigned the admin role; all subsequent users receive viewer.

Request body:

{
  "email": "user@example.com",
  "password": "secure-password",
  "display_name": "Alice"
}
FieldTypeRequiredDescription
emailstring (email)yesUser email address
passwordstringyesPassword (hashed with Argon2)
display_namestringnoDisplay name

Response (201):

{
  "user_id": "uuid",
  "email": "user@example.com",
  "role": "admin"
}

POST /api/v1/auth/login

Authenticate and receive a session cookie (HttpOnly JWT, 24h expiry).

Request body:

{
  "email": "user@example.com",
  "password": "secure-password"
}

Response (200):

{
  "user_id": "uuid",
  "email": "user@example.com",
  "role": "admin",
  "display_name": "Alice"
}

Sets cookie: audittrail_session (HttpOnly, SameSite=Lax, max-age=86400).

POST /api/v1/auth/logout

Clear the session cookie. No request body required.

Response: 204 No Content

GET /api/v1/auth/me

Get the authenticated user's profile. Requires session cookie.

Response (200):

{
  "user_id": "uuid",
  "email": "user@example.com",
  "role": "admin",
  "display_name": "Alice",
  "created_at": "2026-03-01T12:00:00Z",
  "last_login_at": "2026-03-30T08:15:00Z"
}

PATCH /api/v1/auth/profile

Update the current user's display name and/or password.

Request body:

{
  "display_name": "Alice B.",
  "password": "new-password"
}

Both fields are optional; at least one must be provided.

Response (200):

{
  "updated": true,
  "fields": ["display_name", "password"]
}

Traces

GET /api/v1/traces

List traces with filtering, sorting, and cursor-based pagination.

Query parameters:

ParameterTypeDefaultDescription
statusstring--Filter by status (complete, error, running)
agent_namestring--Filter by agent name
environmentstring--Filter by environment
modelstring--Filter traces containing spans with this model
created_afterdatetime--ISO 8601 lower bound
created_beforedatetime--ISO 8601 upper bound
searchstring--Search agent name or trace ID (case-insensitive)
sort_bystringcreated_atColumn to sort by
sort_orderstringdescasc or desc
limitinteger50Page size (1-200)
cursorstring--Pagination cursor from previous response

Response (200):

{
  "data": [
    {
      "id": "trace-uuid",
      "session_id": "session-uuid",
      "root_span_id": "span-uuid",
      "agent_name": "researcher",
      "environment": "production",
      "status": "complete",
      "created_at": "2026-03-30T10:00:00Z",
      "completed_at": "2026-03-30T10:00:05Z",
      "total_duration_ms": 5200,
      "total_tokens_in": 1200,
      "total_tokens_out": 450,
      "total_cost": 0.0285,
      "metadata_": {"tags": {"version": "1.0"}},
      "span_count": 6,
      "tools_used": null
    }
  ],
  "meta": {
    "total": 150,
    "cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMy0zMFQxMDowMDowMFoiLCJpZCI6InRyYWNlLXV1aWQifQ==",
    "has_more": true
  }
}

GET /api/v1/traces/{trace_id}

Get a single trace with summary statistics including tools used.

Response (200):

{
  "data": {
    "id": "trace-uuid",
    "session_id": "session-uuid",
    "root_span_id": "span-uuid",
    "agent_name": "researcher",
    "environment": "production",
    "status": "complete",
    "created_at": "2026-03-30T10:00:00Z",
    "completed_at": "2026-03-30T10:00:05Z",
    "total_duration_ms": 5200,
    "total_tokens_in": 1200,
    "total_tokens_out": 450,
    "total_cost": 0.0285,
    "metadata_": {},
    "span_count": 6,
    "tools_used": ["web_search", "calculator"]
  }
}

DELETE /api/v1/traces/{trace_id}

Delete a trace and all associated data (spans, tool calls, evaluations).

Response (200):

{
  "success": true
}

DELETE /api/v1/traces

Bulk-delete traces by ID list.

Request body:

{
  "ids": ["trace-uuid-1", "trace-uuid-2"]
}

Response (200):

{
  "data": {
    "deleted": 2
  }
}

DELETE /api/v1/traces/flush-all

Delete ALL traces in the database. Admin role required. For clearing development/seed data.

Response (200):

{
  "data": {
    "deleted": 26
  }
}
StatusDescription
200All traces deleted
401Not authenticated
403Admin role required

GET /api/v1/traces/{trace_id}/dag

Get the reconstructed decision DAG for a trace, formatted for React Flow.

Response (200):

{
  "data": {
    "nodes": [
      {
        "id": "span-uuid",
        "type": "llmNode",
        "position": {"x": 0, "y": 0},
        "data": {
          "span_id": "span-uuid",
          "name": "gpt-4o-step-0",
          "span_type": "llm",
          "status": "ok",
          "duration_ms": 1200,
          "tokens_in": 350,
          "tokens_out": 120,
          "cost": 0.0071,
          "model": "gpt-4o",
          "tool_name": null,
          "selected_from": null,
          "has_children": true,
          "child_count": 2,
          "children_count": 2,
          "is_critical_path": true,
          "tool_calls": null,
          "depth": 0
        }
      }
    ],
    "edges": [
      {
        "id": "edge-uuid",
        "source": "span-uuid-1",
        "target": "span-uuid-2",
        "type": "default",
        "animated": false,
        "style": null
      }
    ],
    "total_nodes": 6
  }
}

GET /api/v1/traces/{trace_id}/spans

Get all spans belonging to a trace, ordered by start time.

Response (200):

{
  "data": [
    {
      "id": "span-uuid",
      "trace_id": "trace-uuid",
      "parent_span_id": null,
      "span_type": "agent",
      "name": "researcher-orchestrator",
      "start_time": "2026-03-30T10:00:00Z",
      "end_time": "2026-03-30T10:00:05Z",
      "duration_ms": 5200,
      "status": "ok",
      "tokens_in": 0,
      "tokens_out": 0,
      "estimated_cost": 0.0,
      "model": null,
      "error_message": null,
      "error_type": null
    }
  ]
}

GET /api/v1/traces/{trace_id}/timeline

Get spans with computed depth for timeline/waterfall rendering.

Response (200):

{
  "data": [
    {
      "id": "span-uuid",
      "trace_id": "trace-uuid",
      "parent_span_id": null,
      "span_type": "agent",
      "name": "researcher-orchestrator",
      "start_time": "2026-03-30T10:00:00Z",
      "end_time": "2026-03-30T10:00:05Z",
      "duration_ms": 5200,
      "status": "ok",
      "tokens_in": 0,
      "tokens_out": 0,
      "estimated_cost": 0.0,
      "model": null,
      "error_message": null,
      "error_type": null,
      "depth": 0
    }
  ]
}

GET /api/v1/traces/facets

Get distinct values for filter dropdowns (environments, models, agents, statuses).

Response (200):

{
  "environment": ["development", "staging", "production"],
  "model": ["gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet"],
  "agent_name": ["researcher", "qa-bot", "data-analyst"],
  "status": ["complete", "error", "running"]
}

GET /api/v1/traces/export

Export filtered traces as a CSV file. Accepts the same filter query parameters as GET /traces (except limit, cursor, sort_by, sort_order).

Query parameters: status, agent_name, environment, model, search, created_after, created_before

Response: text/csv file download with headers: id, agent_name, environment, status, created_at, completed_at, total_duration_ms, total_tokens_in, total_tokens_out, total_cost


Spans

GET /api/v1/spans/{span_id}

Get a single span with full metadata including input, output, and attributes.

Response (200):

{
  "data": {
    "id": "span-uuid",
    "trace_id": "trace-uuid",
    "parent_span_id": "parent-span-uuid",
    "span_type": "llm",
    "name": "gpt-4o-step-1",
    "start_time": "2026-03-30T10:00:01Z",
    "end_time": "2026-03-30T10:00:02Z",
    "duration_ms": 1200,
    "status": "ok",
    "tokens_in": 350,
    "tokens_out": 120,
    "estimated_cost": 0.0071,
    "model": "gpt-4o",
    "error_message": null,
    "error_type": null,
    "input": {"prompt": "Research the latest AI safety papers"},
    "output": {"response": "Here are the top papers..."},
    "attributes": {"temperature": 0.7}
  }
}

GET /api/v1/spans/{span_id}/tool-calls

Get all tool calls associated with a span.

Response (200):

{
  "data": [
    {
      "id": "toolcall-uuid",
      "span_id": "span-uuid",
      "tool_name": "web_search",
      "arguments": {"query": "AI safety papers 2026"},
      "result": {"data": "..."},
      "duration_ms": 800,
      "selected_from": ["web_search", "calculator", "read_file"],
      "selection_confidence": 0.92
    }
  ]
}

Ingest

These endpoints are the data-plane entry point for instrumented agents. Spans are upserted (idempotent, at-least-once delivery). Traces and agents are auto-created as needed.

POST /api/v1/ingest/spans

Batch-ingest spans. Maximum batch size: 500. Also runs constitutional rule evaluation on completed spans and broadcasts violations via WebSocket.

Request body:

{
  "batch": [
    {
      "span_id": "span-uuid",
      "trace_id": "trace-uuid",
      "parent_span_id": "parent-uuid",
      "name": "gpt-4o-step-0",
      "span_type": "llm",
      "start_time": "2026-03-30T10:00:00Z",
      "end_time": "2026-03-30T10:00:01Z",
      "status": "ok",
      "model": "gpt-4o",
      "input": {"prompt": "..."},
      "output": {"response": "..."},
      "error_message": null,
      "tokens_in": 350,
      "tokens_out": 120,
      "cost": 0.0071,
      "tool_calls": [
        {
          "tool_name": "web_search",
          "arguments": {"query": "..."},
          "result": {"data": "..."},
          "selected_from": ["web_search", "calculator"]
        }
      ],
      "attributes": {"temperature": 0.7}
    }
  ]
}
FieldTypeRequiredDescription
span_idstringyesUnique span identifier
trace_idstringyesParent trace identifier
parent_span_idstringnoParent span (for tree structure)
namestringyesSpan name
span_typestringyesllm, tool, agent, chain, retriever, embedding, custom
start_timedatetimeyesISO 8601 timestamp
end_timedatetimenoISO 8601 timestamp (null if still running)
statusstringnorunning, ok, complete, error (default: running)
modelstringnoLLM model name
inputobjectnoSpan input data
outputobjectnoSpan output data
error_messagestringnoError description
tokens_inintegernoInput token count
tokens_outintegernoOutput token count
costfloatnoEstimated cost in USD
tool_callsarraynoEmbedded tool call records (see schema below)
attributesobjectnoArbitrary key-value metadata

ToolCallInSpan schema:

Each entry in tool_calls must match:

FieldTypeRequiredDescription
tool_namestringyesName of the tool invoked
argumentsobjectyesArguments passed to the tool (must be a JSON object, not a string)
resultobject / nullnoTool result (must be a JSON object like {"output": "..."}, not a plain string — strings will cause a 422 validation error)
selected_fromarray / nullnoList of candidate tool names the agent chose from

Common mistake: Passing a string for result instead of a dict. Always wrap string results:

{
  "tool_name": "calculator",
  "arguments": {"expression": "2 + 2"},
  "result": {"output": "4"},
  "selected_from": ["calculator", "web_search"]
}

Response (200):

{
  "accepted": 5,
  "errors": null
}

If some spans fail, errors contains descriptions:

{
  "accepted": 3,
  "errors": ["span abc123: duplicate key violation"]
}

POST /api/v1/ingest/traces

Create or update a trace record and auto-register the agent.

Request body:

{
  "trace": {
    "trace_id": "trace-uuid",
    "session_id": "session-uuid",
    "agent_name": "researcher",
    "agent_version": "1.2.0",
    "environment": "production",
    "tags": {"team": "ml-ops"},
    "metadata": {"experiment_id": "exp-42"}
  }
}

Response (200):

{
  "trace_id": "trace-uuid"
}

Ablation (Causal Attribution)

GET /api/v1/ablation/by-trace/{trace_id}

Get the most recent ablation result for a given trace. Used by the frontend to check if ablation results already exist.

Response (200):

{
  "data": {
    "job_id": "uuid",
    "span_id": "uuid",
    "trace_id": "uuid",
    "status": "completed",
    "num_segments": 5,
    "actual_cost": 0.015,
    "ablation_model": "demo",
    "created_at": "2026-03-30T12:00:00",
    "completed_at": "2026-03-30T12:00:05",
    "has_sankey": true,
    "has_shap": true
  }
}

Returns { "data": null } if no ablation exists for this trace.

POST /api/v1/ablation/estimate

Estimate the cost and segment count for an ablation analysis without running it.

Request body:

{
  "span_id": "span-uuid",
  "trace_id": "trace-uuid"
}

Response (200):

{
  "data": {
    "span_id": "span-uuid",
    "trace_id": "trace-uuid",
    "prompt_preview": "Research the latest AI safety papers and summarize...",
    "tool_selected": "web_search",
    "candidates": ["web_search", "calculator", "read_file"],
    "num_segments": 5,
    "segments_preview": [
      {"id": "seg-0", "text": "Research the latest"},
      {"id": "seg-1", "text": "AI safety papers"}
    ],
    "estimated_cost": 0.0,
    "estimated_runs": 15,
    "estimated_tokens": 7500
  }
}

POST /api/v1/ablation/run

Start an ablation analysis job. Requires confirmed: true (use /estimate first to preview cost).

Request body:

{
  "span_id": "span-uuid",
  "trace_id": "trace-uuid",
  "confirmed": true
}

Response (200):

{
  "data": {
    "job_id": "ablation-uuid",
    "status": "complete",
    "cached": false
  }
}

If a cached result exists for the same prompt+model combination:

{
  "data": {
    "job_id": "existing-uuid",
    "status": "complete",
    "cached": true,
    "message": "Returning cached result for identical prompt+model combination."
  }
}

GET /api/v1/ablation/{job_id}

Get the status and results of an ablation job.

Response (200):

{
  "data": {
    "job_id": "ablation-uuid",
    "span_id": "span-uuid",
    "trace_id": "trace-uuid",
    "status": "complete",
    "num_segments": 5,
    "estimated_cost": 0.0,
    "actual_cost": 0.0,
    "ablation_model": "demo",
    "prompt_hash": "abc123...",
    "surrogate_f1": 0.85,
    "created_at": "2026-03-30T10:00:00Z",
    "completed_at": "2026-03-30T10:00:03Z",
    "error_message": null,
    "segments": [...],
    "segment_results": [
      {
        "segment_id": "seg-0",
        "segment_text": "Research the latest",
        "attribution_score": 0.45,
        "original_tool": "web_search",
        "ablated_tool": "web_search",
        "tool_changed": false,
        "original_scores": {"web_search": 0.8, "calculator": 0.1, "read_file": 0.1},
        "ablated_scores": {"web_search": 0.5, "calculator": 0.3, "read_file": 0.2}
      }
    ],
    "tool_selected": "web_search",
    "candidates": ["web_search", "calculator", "read_file"]
  }
}

GET /api/v1/ablation/{job_id}/sankey

Get Sankey diagram data for a completed ablation job. Returns d3-sankey compatible format. Only available when job status is complete.

Response (200):

{
  "data": {
    "nodes": [
      {"id": "phrase-0", "name": "Research the latest", "category": "phrase"},
      {"id": "reasoning-0", "name": "information retrieval", "category": "reasoning"},
      {"id": "tool-web_search", "name": "web_search", "category": "tool"}
    ],
    "links": [
      {"source": "phrase-0", "target": "reasoning-0", "value": 0.45},
      {"source": "reasoning-0", "target": "tool-web_search", "value": 0.45}
    ]
  }
}

Error (409): Returned if the job is not yet complete.

GET /api/v1/ablation/{job_id}/shap

Get SHAP feature importance values from the surrogate model. Only available when job status is complete.

Response (200):

{
  "data": {
    "features": [
      {"name": "prompt_length", "importance": 0.32},
      {"name": "has_question_mark", "importance": 0.18}
    ],
    "surrogate_f1": 0.85,
    "tool_predicted": "web_search",
    "tool_probabilities": {
      "web_search": 0.78,
      "calculator": 0.15,
      "read_file": 0.07
    },
    "is_mock": true
  }
}

Rules (Constitutional)

GET /api/v1/rules

List all loaded constitutional rules.

Response (200):

{
  "data": [
    {
      "id": "no-bulk-delete-001",
      "name": "Bulk delete prevention",
      "description": "Flag tool invocations that perform destructive file-system operations",
      "version": "1.0",
      "target_span_type": "tool",
      "condition_field": "tool_name",
      "condition_operator": "in",
      "thresholds": {
        "amber": {
          "value": ["delete", "rm", "rmdir", "remove", "unlink"],
          "message": "Tool invocation involves a destructive operation"
        },
        "red": {
          "value": ["delete", "rm", "rmdir", "remove", "unlink"],
          "message": "VIOLATION: Destructive file-system operation detected"
        }
      },
      "enabled": true,
      "policy_group": "safety"
    }
  ],
  "total": 6
}

GET /api/v1/rules/{rule_id}

Get a single rule by ID.

Response (200):

{
  "data": {
    "id": "no-bulk-delete-001",
    "name": "Bulk delete prevention",
    "description": "...",
    "version": "1.0",
    "target_span_type": "tool",
    "condition_field": "tool_name",
    "condition_operator": "in",
    "thresholds": {...},
    "enabled": true,
    "policy_group": "safety"
  }
}

POST /api/v1/rules/validate

Parse and validate YAML rule content without loading it into the running system.

Request body:

{
  "yaml_content": "rule:\n  id: test-rule\n  name: Test\n  ..."
}

Response (200):

{
  "valid": true,
  "rule": {
    "id": "test-rule",
    "name": "Test",
    ...
  },
  "error": null
}

If invalid:

{
  "valid": false,
  "rule": null,
  "error": "Missing required field: thresholds"
}

POST /api/v1/rules

Create a new constitutional rule from YAML content.

Request body:

{
  "yaml_content": "rule:\n  id: my-rule-001\n  name: My Rule\n  ..."
}

Response (201):

{
  "data": {
    "id": "my-rule-001",
    "name": "My Rule",
    "policy_group": "safety",
    "enabled": true,
    "conditions": [...]
  }
}

Errors: 422 if YAML is invalid, 409 if rule ID already exists.

PATCH /api/v1/rules/{rule_id}

Update mutable fields of a loaded rule (enabled, name, policy_group).

Request body (all optional):

{
  "enabled": false,
  "name": "Updated Name",
  "policy_group": "operational"
}

Response (200): Updated rule object in { "data": { ... } } wrapper.

Errors: 404 if rule not found.

DELETE /api/v1/rules/{rule_id}

Remove a rule from the loaded rules list.

Response: 204 No Content

Errors: 404 if rule not found.

POST /api/v1/rules/reload

Reload all rules from the configured rules directory (AUDITTRAIL_RULES_DIR).

Response (200):

{
  "reloaded": 6,
  "rule_ids": ["no-bulk-delete-001", "cost-guard-001", "latency-slo-001", ...]
}

Evaluations (Constitutional)

GET /api/v1/evaluations

List constitutional evaluation results with filtering and offset-based pagination.

Query parameters:

ParameterTypeDefaultDescription
trace_idstring--Filter by trace
rule_idstring--Filter by rule
severitystring--Filter by severity (info, amber, red)
passedboolean--Filter by pass/fail
limitinteger50Page size (1-500)
offsetinteger0Offset for pagination

Response (200):

{
  "data": [
    {
      "id": "eval-uuid",
      "span_id": "span-uuid",
      "trace_id": "trace-uuid",
      "rule_id": "no-bulk-delete-001",
      "rule_name": "Bulk delete prevention",
      "rule_version": "1.0",
      "severity": "amber",
      "passed": true,
      "score": 0.8,
      "threshold_amber": 0.8,
      "threshold_red": 1.0,
      "actual_value": "delete_file",
      "explanation": "Tool name matches destructive operation pattern",
      "evaluated_at": "2026-03-30T10:00:02Z",
      "reviewed": false
    }
  ],
  "total": 42
}

GET /api/v1/evaluations/summary

Get compliance summary statistics. Optionally scoped to a single trace.

Query parameters:

ParameterTypeDefaultDescription
trace_idstring--Scope summary to a single trace

Response (200):

{
  "total": 120,
  "passed": 110,
  "amber": 8,
  "red": 2,
  "compliance_percentage": 91.67,
  "by_rule": [
    {
      "rule_id": "no-bulk-delete-001",
      "rule_name": "Bulk delete prevention",
      "total": 15,
      "amber": 3,
      "red": 1
    }
  ]
}

PATCH /api/v1/evaluations/{eval_id}

Mark a constitutional evaluation as reviewed. Appends a review marker to the explanation field.

Request body:

{
  "reviewed": true,
  "notes": "Reviewed by compliance team, false positive"
}

Response (200):

{
  "id": "eval-uuid",
  "reviewed": true,
  "notes": "Reviewed by compliance team, false positive"
}

Analytics

All analytics endpoints cache results for 60 seconds. Repeated requests within the cache window are served from memory.

GET /api/v1/analytics/overview

Dashboard metric cards with high-level statistics.

Query parameters:

ParameterTypeDefaultDescription
created_afterdatetime--ISO 8601 lower bound
created_beforedatetime--ISO 8601 upper bound

Response (200):

{
  "total_runs": 150,
  "error_rate": 4.67,
  "avg_latency_ms": 2340.5,
  "token_usage": 45000,
  "compliance_score": 95.0
}

GET /api/v1/analytics/cost

Cost breakdown grouped by agent, model, or tool.

Query parameters:

ParameterTypeDefaultDescription
group_bystringagentagent, model, or tool
created_afterdatetime--ISO 8601 lower bound
created_beforedatetime--ISO 8601 upper bound

Response (200):

{
  "data": [
    {
      "name": "researcher",
      "total_cost": 1.245,
      "call_count": 45,
      "avg_cost": 0.027667
    }
  ]
}

GET /api/v1/analytics/latency

Latency percentiles (p50, p95, p99) grouped by day.

Query parameters:

ParameterTypeDefaultDescription
created_afterdatetime--ISO 8601 lower bound
created_beforedatetime--ISO 8601 upper bound

Response (200):

{
  "timestamps": ["2026-03-28", "2026-03-29", "2026-03-30"],
  "p50": [1200.0, 1350.5, 1100.0],
  "p95": [3200.0, 3500.0, 2900.0],
  "p99": [5100.0, 5800.0, 4200.0]
}

GET /api/v1/analytics/tools

Tool usage statistics including call counts, success/failure, and average duration.

Query parameters:

ParameterTypeDefaultDescription
created_afterdatetime--ISO 8601 lower bound
created_beforedatetime--ISO 8601 upper bound

Response (200):

{
  "data": [
    {
      "tool_name": "web_search",
      "call_count": 85,
      "success_count": 82,
      "failure_count": 3,
      "avg_duration_ms": 450.25
    }
  ]
}

GET /api/v1/analytics/cost-timeseries

Return daily cost totals for the cost-over-time line chart.

Query params: created_after (ISO datetime, optional), created_before (ISO datetime, optional)

Response (200):

{
  "data": [
    { "date": "Mar 28", "value": 0.0342 },
    { "date": "Mar 29", "value": 0.0518 }
  ]
}

GET /api/v1/analytics/tools-timeseries

Return daily tool call counts for the tool-calls-over-time line chart.

Query params: created_after (ISO datetime, optional), created_before (ISO datetime, optional)

Response (200):

{
  "data": [
    { "date": "Mar 28", "value": 12 },
    { "date": "Mar 29", "value": 8 }
  ]
}

GET /api/v1/analytics/slowest-spans

Return spans with the highest latency, grouped by name with p95/p99 percentiles.

Query params: limit (int, 1-20, default 5)

Response (200):

{
  "data": [
    { "name": "web_search", "p95": 1200.5, "p99": 1850.2, "count": 45 },
    { "name": "calculator", "p95": 80.1, "p99": 120.3, "count": 22 }
  ]
}

Reports

POST /api/v1/reports/generate

Generate a PDF audit report for a specified date range. Runs synchronously in v1.0.

Request body:

{
  "period_start": "2026-03-01",
  "period_end": "2026-03-30",
  "include_violations": true,
  "include_analytics": true
}
FieldTypeDefaultDescription
period_startdaterequiredReport period start (YYYY-MM-DD)
period_enddaterequiredReport period end (YYYY-MM-DD)
include_violationsbooleantrueInclude constitutional violation details
include_analyticsbooleantrueInclude analytics charts and tables

Response (201):

{
  "report_id": "report-uuid",
  "status": "completed"
}

GET /api/v1/reports

List generated reports, newest first.

Query parameters:

ParameterTypeDefaultDescription
limitinteger50Page size (1-200)
offsetinteger0Offset for pagination

Response (200):

{
  "data": [
    {
      "id": "report-uuid",
      "status": "completed",
      "period_start": "2026-03-01",
      "period_end": "2026-03-30",
      "include_violations": true,
      "include_analytics": true,
      "file_path": "data/reports/audit_report_uuid.pdf",
      "file_size_bytes": 52400,
      "generation_time_ms": 1200,
      "summary": {"pdf_size_bytes": 52400, "generation_time_ms": 1200},
      "created_at": "2026-03-30T12:00:00Z",
      "completed_at": "2026-03-30T12:00:01Z",
      "error_message": null
    }
  ],
  "total": 5
}

GET /api/v1/reports/{report_id}

Get the status and metadata of a specific report.

Response (200): Same shape as a single item from the list endpoint.

GET /api/v1/reports/{report_id}/download

Download the generated PDF. Only available when report status is completed.

Response: application/pdf file stream.

Error (409): Returned if the report is not yet ready.


Settings

GET /api/v1/settings

Get all settings as a flat key-value dictionary.

Response (200):

{
  "data": {
    "pii_redaction_enabled": true,
    "retention_days": 90,
    "theme": "dark"
  }
}

GET /api/v1/settings/metrics

Return performance metrics for the settings dashboard (middleware overhead, API latency, WebSocket latency, rule counts).

Response (200):

{
  "middleware_overhead_ms": 2.1,
  "api_response_p95_ms": 52.3,
  "websocket_latency_ms": 10.5,
  "loaded_rules": 6,
  "active_rules": 5,
  "disabled_rules": 1
}

PATCH /api/v1/settings

Update settings. Body is a dictionary of key-value pairs to upsert.

Request body:

{
  "retention_days": 30,
  "theme": "light"
}

Response (200): Returns the full updated settings dictionary (same shape as GET).


Instance

GET /api/v1/instance

Get instance metadata: version, connected agents, rule count, and uptime.

Response (200):

{
  "version": "1.0.0",
  "connected_agents": 3,
  "rules_count": 6,
  "uptime_seconds": 86400.0
}

Agents

GET /api/v1/agents

List registered agents with status and trace count.

Query parameters:

ParameterTypeDefaultDescription
limitinteger50Page size (1-200)
offsetinteger0Offset for pagination

Response (200):

{
  "data": [
    {
      "id": "agent-uuid",
      "name": "researcher",
      "framework": "langgraph",
      "description": "Researches topics via web",
      "first_seen_at": "2026-03-01T00:00:00Z",
      "last_seen_at": "2026-03-30T10:00:00Z",
      "trace_count": 45,
      "status": "online",
      "registered_via": "auto",
      "metadata_": null
    }
  ],
  "total": 3
}

POST /api/v1/agents

Manually register an agent. Agent names must be unique.

Request body:

{
  "name": "my-agent",
  "framework": "langgraph",
  "description": "Custom agent for data processing"
}

Response (201): Same shape as a single item from the list endpoint.

Error (409): Returned if the agent name is already registered.

GET /api/v1/agents/{agent_id}

Get a single agent by ID.

Response (200): Same shape as a single item from the list endpoint.

DELETE /api/v1/agents/{agent_id}

Remove an agent registration.

Response: 204 No Content


API Keys

GET /api/v1/api-keys

List all API keys with secrets masked (only prefix shown).

Response (200):

{
  "data": [
    {
      "id": "key-uuid",
      "name": "production-ingest",
      "public_key": "pk-at-abc123...",
      "secret_key_prefix": "sk-at-def456",
      "scope": "ingest",
      "created_at": "2026-03-01T00:00:00Z",
      "last_used_at": "2026-03-30T10:00:00Z",
      "revoked_at": null
    }
  ],
  "total": 2
}

POST /api/v1/api-keys

Generate a new API key pair. The plaintext secret key is returned only once in this response.

Request body:

{
  "name": "production-ingest",
  "scope": "ingest"
}
FieldTypeDefaultDescription
namestringrequiredHuman-readable key name
scopestringingestPermission scope

Response (201):

{
  "id": "key-uuid",
  "name": "production-ingest",
  "public_key": "pk-at-abc123def456...",
  "secret_key": "sk-at-789xyz...",
  "scope": "ingest",
  "created_at": "2026-03-30T12:00:00Z"
}

Save the secret_key immediately -- it is not retrievable after this response.

DELETE /api/v1/api-keys/{key_id}

Revoke an API key. The key record is not deleted -- it remains visible with a revoked_at timestamp for audit purposes.

Response: 204 No Content

Error (409): Returned if the key is already revoked.


WebSocket

WS /ws/traces/{trace_id}

Subscribe to real-time events for a specific trace. On connect, the client receives a replay of the last 100 events from the ring buffer.

WS /ws/live

Subscribe to ALL trace events across all traces. Used by the dashboard overview for a live activity stream.

Client Messages

Send JSON messages to the WebSocket:

{"type": "ping"}
{"type": "subscribe", "trace_id": "trace-uuid"}
{"type": "unsubscribe", "trace_id": "trace-uuid"}

Server Events

Events are JSON objects with a type field:

Span event:

{
  "type": "span_started",
  "trace_id": "trace-uuid",
  "span_id": "span-uuid",
  "parent_span_id": "parent-uuid",
  "name": "gpt-4o-step-0",
  "span_type": "llm",
  "timestamp": "2026-03-30T10:00:00Z",
  "status": "running",
  "duration_ms": null,
  "error_message": null,
  "error_type": null,
  "model": "gpt-4o",
  "tokens_in": null,
  "tokens_out": null,
  "estimated_cost": null
}

Constitutional violation event:

{
  "type": "constitutional_violation",
  "trace_id": "trace-uuid",
  "span_id": "span-uuid",
  "rule_id": "no-bulk-delete-001",
  "rule_name": "Bulk delete prevention",
  "severity": "amber",
  "passed": true,
  "explanation": "Tool name matches destructive operation pattern",
  "actual_value": "delete_file",
  "evaluated_at": "2026-03-30T10:00:02Z"
}

Error Responses

All error responses follow a consistent format:

{
  "detail": "Resource not found",
  "code": "not_found"
}
StatusCodeDescription
400variesBad request (validation error, invalid cursor, etc.)
401--Unauthorized (missing or invalid session)
403--Forbidden (deactivated account)
404not_foundResource not found
409--Conflict (duplicate email, already revoked, not ready)
422--Unprocessable entity (cannot extract prompt for ablation)
500internal_errorInternal server error