AuditTrail API Reference
Complete REST + WebSocket surface for AuditTrail v3.45.0.
Base URL: /api/v1 (all REST routes; the SCIM provisioning endpoints are the
only exception — they live under /scim/v2 per the SCIM spec). Request and
response bodies are JSON unless noted (CSV / PDF exports stream files).
Authentication & authorization
AuditTrail accepts several credential types depending on the route group:
| Credential | Header / mechanism | Used by |
|---|---|---|
| Session cookie | audittrail_session (HttpOnly JWT, set by POST /auth/login) | Dashboard / browser |
| Ingest API key | Authorization: Bearer sk-at-… | Agent SDKs, OTLP exporters |
| Gateway virtual key | Authorization: Bearer at-gw-… | Gateway proxy only |
| Runner token | Authorization: Bearer sk-atd-… (or ?token=) | Local runner daemon (WS + GET /runner/status) |
| SCIM token | Authorization: Bearer scim-… | IdP SCIM provisioning under /scim/v2 |
| CSRF double-submit | audittrail_csrf cookie echoed in a header | All cookie-auth mutations |
The dependency names you'll see referenced below map to:
get_current_user— session cookie required.require_user_or_apikey— session cookie or ansk-at-…ingest key.require_user_apikey_or_runner_token— adds the daemon'ssk-atd-…token.require_admin— tenantadminrole.require_superadmin— cross-tenantis_superadminflag.
User isolation (Hard Rule 8): every owned row is filtered by
user_id == current_user.id. Cross-tenant reads return 404 (existence is
not leaked) on the newer route groups and 403 on a few older ones — the
purpose column notes where this matters.
Response envelope: trace / span / DAG / evaluation / ablation GETs wrap their
payload in {"data": …}. Read resp.json()["data"], not resp.json(). Newer
route groups (alerts, datasets, evals, agent-registry, runner, organizations,
SSO/SCIM, compliance) return bare JSON or a typed object.
Auth & identity
routes/auth.py — prefix /api/v1/auth. Login / register are rate-limited
(10/minute); password reset is tighter (5/minute).
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /auth/register | none | Create an account (first user → admin, rest → viewer) |
POST | /auth/login | none | Authenticate; sets session + CSRF + refresh cookies |
POST | /auth/logout | cookie (best-effort) | Clear cookies, revoke refresh tokens, audit-log |
GET | /auth/me | cookie | Current user profile |
PATCH | /auth/profile | cookie | Update display name and/or password |
POST | /auth/forgot-password | none | Issue a reset token (logged server-side in v1.x) |
POST | /auth/reset-password | none | Consume a reset token, set new password |
POST | /auth/verify-email | none | Consume an email-verification token |
POST | /auth/resend-verification | cookie | Re-issue a verification token |
POST | /auth/refresh | refresh cookie | Rotate refresh token + mint a new access JWT (theft-chain detection) |
POST | /auth/demo-login | none | One-click public demo user (seeds sample data on first use) |
POST /auth/login request:
{ "email": "user@example.com", "password": "secure-password" }Response (200):
{ "user_id": "uuid", "email": "user@example.com", "role": "admin", "display_name": "Alice" }Sets audittrail_session (HttpOnly, SameSite=Strict, 24h), audittrail_csrf
(JS-readable, SameSite=Lax), and audittrail_refresh (HttpOnly, scoped to
/api/v1/auth, 30d).
Admin (superadmin, cross-tenant)
routes/admin.py — prefix /api/v1/admin. Every route requires
require_superadmin; mutations write audit-log rows.
| Method | Path | Purpose |
|---|---|---|
GET | /admin/users | List every user across tenants |
GET | /admin/users/{user_id} | One user's detail |
PATCH | /admin/users/{user_id} | Promote/demote or (de)activate (self-protection enforced) |
DELETE | /admin/users/{user_id} | Soft-delete (is_active=False) |
GET | /admin/audit-log | Paginated audit-log entries |
GET | /admin/audit-log/export | Stream the full audit log as CSV |
GET | /admin/tenants | List tenants (role=admin users) + trace counts |
GET | /admin/instance/info | Instance-wide stats |
GET | /admin/database | SQLite health: on-disk db/WAL/-shm sizes, read-only PRAGMA counters (page/freelist/journal-mode), per-table row counts busiest-first (v3.36.0; strictly read-only — degrades honestly on in-memory/non-SQLite backends). UI: /admin/database |
Organizations, members, invites
routes/organizations.py — prefix /api/v1/organizations. RBAC ranks
owner > admin > annotator > viewer via require_org_role. See
SSO & SCIM for the SAML/SCIM surface that sits on top.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /organizations | user/apikey | List orgs the caller belongs to |
POST | /organizations | user/apikey | Create an org (caller becomes owner) |
GET | /organizations/{org_id} | member | Org detail + your role |
PATCH | /organizations/{org_id} | admin+ | Update (plan change is owner-only) |
GET | /organizations/{org_id}/members | member | List members |
PATCH | /organizations/{org_id}/members/{member_id} | admin+ | Change role (can't demote last owner) |
DELETE | /organizations/{org_id}/members/{member_id} | admin+ | Remove member (can't remove last owner) |
POST | /organizations/{org_id}/invites | admin+ | Issue an invite (one-time token) |
GET | /organizations/{org_id}/invites | admin+ | List invites |
DELETE | /organizations/{org_id}/invites/{invite_id} | admin+ | Revoke an invite |
POST | /organizations/accept-invite | user/apikey | Accept an invite token (matched on caller email) |
Projects
routes/projects.py — prefix /api/v1/organizations/{org_id}/projects.
| Method | Path | Purpose |
|---|---|---|
GET | …/projects | List projects in an org |
POST | …/projects | Create a project |
GET | …/projects/{project_id} | Project detail |
PATCH | …/projects/{project_id} | Update a project |
DELETE | …/projects/{project_id} | Delete a project |
API keys
routes/api_keys.py — prefix /api/v1/api-keys. The plaintext sk-at-…
secret is shown once on create.
| Method | Path | Purpose |
|---|---|---|
GET | /api-keys | List keys (secret masked to prefix) |
POST | /api-keys | Create an ingest key pair (returns the one-time secret) |
DELETE | /api-keys/{key_id} | Revoke a key |
Traces & spans
routes/traces.py + routes/spans.py — prefix /api/v1. All
get_current_user-scoped; GETs use the {"data": …} envelope.
| Method | Path | Purpose |
|---|---|---|
GET | /traces | List with filters + cursor pagination |
GET | /traces/facets | Distinct values for filter dropdowns |
GET | /traces/export | Export filtered traces as CSV (≤10k rows) |
GET | /traces/{trace_id} | One trace + summary stats + tools used |
DELETE | /traces/flush-all | admin — delete all of the caller's traces |
DELETE | /traces/{trace_id} | Delete one trace (cascade) |
DELETE | /traces | Bulk-delete by ID list |
GET | /traces/{trace_id}/dag | Reconstructed decision DAG (React Flow shape) |
GET | /traces/{trace_id}/spans | All spans on a trace |
GET | /traces/{trace_id}/timeline | Spans + computed depth (waterfall) |
GET | /spans/{span_id} | One span with full input/output/attributes |
GET | /spans/{span_id}/tool-calls | Tool calls attached to a span |
GET /traces query parameters: status, agent_name, environment,
model, tool (traces with at least one tool span of that name — v3.26.0),
created_after, created_before, search, sort_by
(created_at default), sort_order (desc), limit (1–200, default 50),
cursor.
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-06-30T10:00:00Z", "completed_at": "2026-06-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
}
],
"meta": { "total": 150, "cursor": "eyJjcmVhdGVkX2F0Ijoi…", "has_more": true }
}The cursor is a base64-encoded {"created_at": <iso>, "id": <str>}; pass it back
as ?cursor= to fetch the next page.
Ingest
routes/ingest.py — prefix /api/v1/ingest. The data-plane entry point.
require_user_or_apikey; spans are upserted on span_id (idempotent,
at-least-once). Each route is rate-limited 60/minute. See the
quickstart for full agent-integration patterns.
| Method | Path | Purpose |
|---|---|---|
POST | /ingest/spans | Batch-ingest spans (≤500), run constitutional eval, broadcast over WS |
POST | /ingest/traces | Create/update a trace + auto-register the agent |
POST | /ingest/otlp | OTLP/JSON ingest — maps gen_ai.* attributes (≤2000 spans, zero-SDK) |
POST /ingest/spans request:
{
"batch": [
{
"span_id": "span-uuid", "trace_id": "trace-uuid", "parent_span_id": null,
"name": "gpt-4o-step-0", "span_type": "llm",
"start_time": "2026-06-30T10:00:00Z", "end_time": "2026-06-30T10:00:01Z",
"status": "ok", "model": "gpt-4o",
"input": {"prompt": "…"}, "output": {"response": "…"},
"tokens_in": 350, "tokens_out": 120, "cost": 0.0071,
"tool_calls": [
{ "tool_name": "web_search", "arguments": {"query": "…"},
"result": {"output": "…"}, "selected_from": ["web_search", "calculator"] }
],
"attributes": {"temperature": 0.7}
}
]
}Response (200): { "accepted": 5, "errors": null }. Partial failures land in
errors. The OTLP endpoint returns { "accepted", "rejected", "partialSuccess?" }.
Zero-SDK OTLP: point an OpenTelemetry exporter at the OTLP route with an
Authorization: Bearer sk-at-… header — gen_ai.* attributes map onto native
span fields. JS/Node exporters can target it directly with
OTEL_EXPORTER_OTLP_PROTOCOL=http/json; the Python OTel SDK does not
implement that protocol option, so bridge through an OpenTelemetry Collector
(otlphttp exporter with encoding: json) — full recipes on
OpenTelemetry integration.
XAI & causal attribution
Ablation (causal attribution)
routes/ablation.py — prefix /api/v1/ablation. get_current_user. Ablation is
opt-in and cached; always preview cost with /estimate first.
| Method | Path | Purpose |
|---|---|---|
GET | /ablation/by-trace/{trace_id} | Most-recent completed result for a trace (or null) |
POST | /ablation/estimate | Cost + segment-count preview (no run) |
POST | /ablation/segments/preview | Auto-segment an arbitrary prompt |
POST | /ablation/run | Start a job (confirmed: true required) |
GET | /ablation/{job_id} | Job status + full result |
GET | /ablation/{job_id}/sankey | d3-sankey data (409 until complete) |
GET | /ablation/{job_id}/shap | SHAP feature importance + tool probabilities |
Counterfactuals & NL explanations
routes/xai.py — prefix /api/v1/xai. require_user_or_apikey, rate-limited
20/minute. See Counterfactuals and
NL Explanations.
| Method | Path | Purpose |
|---|---|---|
POST | /xai/explain-attributions | Natural-language "why this tool?" summary for a span |
POST | /xai/counterfactuals | "What input would have changed the decision?" candidates |
SAE mechanistic XAI
routes/v20.py (sae_router) — prefix /api/v1/sae. require_user_or_apikey.
See SAE mechanistic XAI.
| Method | Path | Purpose |
|---|---|---|
GET | /sae/supported-models | Catalog of SAE-supported models for the UI empty state |
GET | /sae/candidate-traces | "Which of my runs support SAE?" — user-scoped traces with ≥1 span on a supported open-weight model (honestly empty on closed-weight demos) |
GET | /sae/span/{span_id} | Per-span SAE state (cached features, config readiness, honest reason) |
POST | /sae/extract | Trigger extraction for a span (gated on saelens+torch+HF key) |
Governance & rules
routes/rules.py — prefix /api/v1. Reads use get_current_user; mutations
require require_admin. Rules live in YAML (Hard Rule 11); a tenant's edits are
stored as overrides. Per-user rule bindings scope which rules fire on which
spans (v2.7.0 E5).
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /rules | cookie | Effective rules for the tenant |
GET | /rules/{rule_id} | cookie | One effective rule |
PATCH | /rules/{rule_id} | admin | Update a tenant override |
DELETE | /rules/{rule_id} | admin | Remove the tenant override |
POST | /rules | admin | Add a tenant-owned override from YAML |
POST | /rules/validate | cookie | Parse/validate YAML without loading |
POST | /rules/reload | admin | Reload instance-default rules from disk |
GET | /rules/{rule_id}/bindings | cookie | List the caller's scope bindings on a rule |
POST | /rules/{rule_id}/bindings | cookie | Attach a scope binding |
DELETE | /rules/{rule_id}/bindings/{binding_id} | cookie | Remove a scope binding |
REGO policies (Governance 2.0)
routes/v20.py (policy_router) — prefix /api/v1/policies.
require_user_or_apikey.
| Method | Path | Purpose |
|---|---|---|
GET | /policies | List the caller's REGO policies |
POST | /policies | Create a policy (inline REGO or external OPA URL) |
POST | /policies/simulate | Evaluate a policy against an input doc |
DELETE | /policies/{policy_id} | Delete a policy |
Constitutional evaluations
routes/evaluations.py — prefix /api/v1. get_current_user.
| Method | Path | Purpose |
|---|---|---|
GET | /evaluations | List evaluation results (filter by trace/rule/severity/passed) |
GET | /evaluations/summary | Compliance summary stats (optionally per-trace) |
PATCH | /evaluations/{eval_id} | Mark an evaluation reviewed |
GET | /constitutional/rules | Rules + evaluation statistics (compliance dashboard) |
GET | /constitutional/summary | Frontend-shaped compliance summary + trend markers |
GET | /constitutional/evaluations | Richer evaluation list for the compliance page |
Evals & datasets
Two distinct surfaces — see Evaluations.
Datasets
routes/datasets.py — prefix /api/v1/datasets. require_user_or_apikey.
| Method | Path | Purpose |
|---|---|---|
GET | /datasets | List datasets (with item counts) |
POST | /datasets | Create a dataset |
GET | /datasets/{dataset_id} | Dataset detail |
DELETE | /datasets/{dataset_id} | Archive a dataset |
GET | /datasets/{dataset_id}/items | List items |
POST | /datasets/{dataset_id}/items | Add an item |
DELETE | /datasets/{dataset_id}/items/{item_id} | Delete an item |
Eval runs
routes/evals.py — prefix /api/v1/evals. require_user_or_apikey. Runs
execute a built-in evaluator over a dataset in a detached task.
| Method | Path | Purpose |
|---|---|---|
GET | /evals/evaluators | Built-in evaluator catalog |
GET | /evals/runs | List the caller's runs |
POST | /evals/runs | Start a run over a dataset |
GET | /evals/runs/{run_id} | Run status + mean score |
GET | /evals/runs/{run_id}/items | Per-item scores |
Annotation queues
routes/annotations.py — prefix /api/v1/annotations. require_user_or_apikey.
| Method | Path | Purpose |
|---|---|---|
GET | /annotations/queues | List annotation queues |
POST | /annotations/queues | Create a queue |
POST | /annotations | Create an annotation |
GET | /annotations/queues/{queue_id} | List annotations in a queue |
Deployments & runtime controls
routes/deployments.py — prefix /api/v1/deployments/actions.
require_user_or_apikey; state-changing routes 30/minute. The 3-tier safety
model and the executor capability matrix are documented in
Deployment actions and
Executable actions; the read-only runtime-control
projection powers Run status.
| Method | Path | Purpose |
|---|---|---|
GET | /deployments/actions/capabilities | Per-action executor capability matrix |
GET | /deployments/actions/runtime-controls | The caller's ACTIVE gateway-enforced controls (read-only) |
POST | /deployments/actions/runtime-controls/{control_id}/deactivate | Manually deactivate an active runtime control (30/minute) |
GET | /deployments/actions | List actions (filter by status) |
POST | /deployments/actions | Propose an action (autonomous Tier-1 auto-executes) |
POST | /deployments/actions/{action_id}/approve | Approve (within TTL) |
POST | /deployments/actions/{action_id}/reject | Reject |
POST | /deployments/actions/{action_id}/mark-executed | Record external execution |
Attribution evidence (ActionOut.evidence, v3.30.0). When a proposal's
target_ref resolves to one of the caller's traces or spans, the propose path
stores the no-LLM surrogate SHAP attribution for the decision-relevant tool
span on the action row: {version, source ("surrogate" | "heuristic"), explanation, top_features[{name, value, shap_value, description}], surrogate_f1, tool, span_id, trace_id, violation{rule_id, rule_name, severity} | null, computed_at}. source is honest provenance — "surrogate" only when
the startup-trained model exists; surrogate_f1 is null on the heuristic
fallback. Actions without a resolvable trace/span target have evidence: null.
Computed best-effort; never blocks or fails the propose.
Runtime-control fields (RuntimeControlOut). Each active control row carries
id, control_type (switch_model / throttle / disable_flag), target,
params_json, source_action_id (the deployment_actions row that created it),
created_at, and expires_at (v3.9.0). expires_at is a nullable TTL — null
means the control is permanent until deactivated; a non-null timestamp is when
the executor's background reaper will auto-deactivate it (fail-safe: an expired
throttle/disable/switch stops enforcing). See Run status for
the TTL and deactivate lifecycle.
Deactivate (v3.10.0) —
POST /deployments/actions/runtime-controls/{control_id}/deactivate flips the
control's active flag to false so the gateway stops enforcing it, then
broadcasts runtime_control.expired on the owner's WebSocket. User-scoped
(Hard Rule 8): only the caller's own active controls are addressable, so a
control that is already inactive or belongs to another tenant returns 404.
Returns the updated RuntimeControlOut.
Agent registry & secrets
Agent registry (immutable versioning)
routes/agent_registry.py — prefix /api/v1/agent-registry.
require_user_or_apikey; cross-tenant → 404; mutations 20/minute. See
Agent registry.
| Method | Path | Purpose |
|---|---|---|
GET | /agent-registry/ | List the caller's definitions |
POST | /agent-registry/ | Create a definition |
GET | /agent-registry/{definition_id} | Definition detail + current version |
DELETE | /agent-registry/{definition_id} | Delete a definition (cascades versions) |
POST | /agent-registry/{definition_id}/versions | Create an immutable, validated version |
GET | /agent-registry/{definition_id}/versions | List versions (newest first) |
GET | /agent-registry/{definition_id}/versions/{version} | One version (full files_json) |
POST | /agent-registry/{definition_id}/promote/{version} | Set the current (default-dispatch) version |
Agent secrets (gateway-only BYOK)
routes/agent_secrets.py — prefix /api/v1/agent-secrets.
require_user_or_apikey; cross-tenant → 404; mutations 30/minute. The plaintext
value is write-only — never returned, never logged. See
Gateway secrets.
| Method | Path | Purpose |
|---|---|---|
GET | /agent-secrets | List secrets (metadata + last4 only) |
POST | /agent-secrets | Store a provider secret (encrypted on receipt) |
PUT | /agent-secrets/{secret_id} | Rotate (re-encrypt) a secret |
DELETE | /agent-secrets/{secret_id} | Delete a secret |
Agent templates & agents
routes/agent_templates.py — prefix /api/v1/agent-templates
(require_user_or_apikey). See Running agents from chat.
| Method | Path | Purpose |
|---|---|---|
GET | /agent-templates | Template catalog |
GET | /agent-templates/{template_id} | One template |
POST | /agent-templates/{template_id}/render | Render a runnable snippet |
GET | /agent-templates/proposals/{correlation_id} | Have traces for this correlation id landed yet? |
routes/agents.py — prefix /api/v1/agents. Reads get_current_user; create /
delete require require_admin.
| Method | Path | Purpose |
|---|---|---|
GET | /agents | List registered agents |
POST | /agents | Register an agent (admin) |
GET | /agents/{agent_id} | One agent |
DELETE | /agents/{agent_id} | Remove an agent (admin) |
Runner daemon
routes/runner.py — prefix /api/v1/runner. require_user_or_apikey (except
/status, which also accepts the daemon's sk-atd-… runner token). Dispatch is
gated by a per-user template allowlist. See Local runner
and Run status.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /runner/tokens | user/apikey | Mint a daemon token (one-time sk-atd-… secret) |
GET | /runner/tokens | user/apikey | List tokens (no secrets) |
DELETE | /runner/tokens/{token_id} | user/apikey | Revoke a token + force-disconnect the daemon |
GET | /runner/status | user/apikey/runner-token | Connection + last-dispatch summary |
POST | /runner/dispatch | user/apikey | Send a run to the connected daemon |
GET | /runner/dispatches/{dispatch_id} | user/apikey | Poll status + stdout tail |
GET | /runner/dispatches/{dispatch_id}/stream | user/apikey | SSE tail of the ring buffer |
DELETE | /runner/dispatches/{dispatch_id} | user/apikey | Cancel a dispatch (best-effort) |
GET | /runner/allowlist | user/apikey | Effective allowlist + addable catalog |
POST | /runner/allowlist | user/apikey | Allowlist a built-in or agent:<id> reference |
DELETE | /runner/allowlist/{entry_id} | user/apikey | Remove an allowlist entry |
POST /runner/dispatch request:
{
"template_id": "quickstart",
"prompt": "Compute 12 * 8 and explain the result",
"model": "gpt-4o-mini",
"env_overrides": {},
"version": null
}template_id is a built-in (quickstart / web-search) or a registered agent
reference (agent:<definition_id>). A value not in the caller's effective
allowlist returns 422 {"error": "template_not_allowed", "allowed": [...]};
no connected daemon returns 404.
Response (201): { "dispatch_id": "…", "correlation_id": "…" }.
Dispatch detail (GET /runner/dispatches/{id} → DispatchDetailOut):
{
"id": "…",
"correlation_id": "…",
"template_id": "agent:…",
"prompt": "…",
"status": "succeeded",
"stdout_tail": [{ "stream": "stdout", "line": "…", "at": "…" }],
"trace_id": "…",
"created_at": "…",
"started_at": "…",
"completed_at": "…",
"error_log": null,
"agent_definition_id": "…",
"agent_version_id": "…",
"agent_version": 3
}The three version-provenance fields — agent_definition_id,
agent_version_id, agent_version (v3.11.0) — record which registered agent
version actually ran. They are null for built-in templates (quickstart /
web-search) and populated only for agent:<id> dispatches. Note the
last-dispatch summary in GET /runner/status (DispatchSummary) carries the
lighter pair agent_definition_id + agent_version only; agent_version_id
appears solely on the full detail response above. See
Local runner.
The daemon connects over WebSocket at /api/v1/runner/ws (below).
Gateway proxy
routes/gateway.py — an OpenAI-compatible reverse proxy that traces every LLM
call. Virtual keys (at-gw-…) are managed under /api/v1/gateway
(require_user_or_apikey); the proxy itself lives under
/api/v1/gateway/proxy/v1 and authenticates with the virtual key as a Bearer
token. See Gateway proxy.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /gateway/keys | user/apikey | Create a virtual key (one-time at-gw-… secret) |
GET | /gateway/keys | user/apikey | List virtual keys |
POST | /gateway/keys/{key_id}/revoke | user/apikey | Revoke a virtual key |
POST | /gateway/proxy/v1/chat/completions | virtual key | OpenAI-compatible chat completions (streaming + non-streaming) |
GET | /gateway/proxy/v1/models | virtual key | List routed model IDs |
Calling the proxy — point any OpenAI SDK at it:
export OPENAI_BASE_URL="https://auditrail.yourco.com/api/v1/gateway/proxy/v1"
export OPENAI_API_KEY="at-gw-YOUR-VIRTUAL-KEY"Optional headers / body fields the proxy understands (stripped before
forwarding upstream): X-AuditTrail-Secret selects a stored
gateway secret as the upstream credential;
audittrail_prompt_key opts the request into
prompt-canary attribution. Active
runtime controls (switch_model / throttle /
disable_flag) are applied to the owner's gateway-routed traffic before the
call forwards. Every completion is logged as an llm span — constitutional
rules fire exactly as for SDK-originated spans.
Unsupported model → 400 (v3.7.0). When a provider key is configured and
the request names a model the gateway doesn't recognise, the proxy refuses with a
400 rather than fabricating a response:
{
"error": "unsupported_model",
"model": "totally-made-up-model",
"supported": ["gpt-*", "o1/o3/o4*", "openai/*", "claude-*", "anthropic/*", "minimax*", "ollama/*", "gemini-*", "qwen*", "mock/* (keyless dev only)"]
}The check runs before both the streaming and non-streaming branches (a stream
can't be un-started once it begins). The keyless dev-mock fallback — no
provider key configured at all — is unaffected and still serves a deterministic
mock/* completion. See Gateway proxy.
Prompts, canary & optimize
routes/prompts.py, routes/prompt_canary.py, routes/prompt_optimize.py —
all under prefix /api/v1/prompts. require_user_or_apikey; cross-tenant → 404.
Canary mutations 10/minute. See Prompt canary.
| Method | Path | Purpose |
|---|---|---|
GET | /prompts | Latest version per prompt key |
POST | /prompts | Create a new prompt version |
GET | /prompts/{prompt_key}/versions | All versions for a key |
GET | /prompts/{prompt_key}/production | The production version |
POST | /prompts/{prompt_key}/production | Promote a version to production |
GET | /prompts/canary/active | All active canary deployments (any status) |
GET | /prompts/{prompt_key}/canary/active | Active deployment for one key |
POST | /prompts/{prompt_key}/canary/propose | Create a proposed deployment |
POST | /prompts/{prompt_key}/canary/start | proposed → ramping |
POST | /prompts/{prompt_key}/canary/ramp | Set weight (10/25/50, clamped to max) |
POST | /prompts/{prompt_key}/canary/pause | ramping → analyzing |
POST | /prompts/{prompt_key}/canary/promote | analyzing → stable (swap production) |
POST | /prompts/{prompt_key}/canary/rollback | Any non-terminal → rolled_back |
POST | /prompts/{prompt_key}/optimize/estimate | Cost preview for an optimizer run |
POST | /prompts/{prompt_key}/optimize | Run the prompt optimizer (confirmed gate) |
Chat sessions & assistant
routes/chat_sessions.py — prefix /api/v1/chat/sessions
(require_user_or_apikey; mutations 20/minute). routes/assistant.py — prefix
/api/v1/assistant. See Operations Assistant.
| Method | Path | Purpose |
|---|---|---|
GET | /chat/sessions | List sessions (archived filter, cursor pagination) |
POST | /chat/sessions | Create an empty session |
GET | /chat/sessions/{session_id} | Session + a page of messages |
PATCH | /chat/sessions/{session_id} | Rename / archive |
DELETE | /chat/sessions/{session_id} | Delete (cascades messages) |
POST | /chat/sessions/{session_id}/messages | Append a message (idempotent on consecutive duplicates) |
GET | /assistant/keys | List BYOK provider keys |
POST | /assistant/keys | Add a provider key |
DELETE | /assistant/keys/{key_id} | Delete a provider key |
POST | /assistant/keys/{key_id}/test | Fire a tiny completion to verify a key |
POST | /assistant/chat | SSE-streamed chat completion (emits generative-UI tiles) |
Compliance
routes/compliance_admin.py — prefix /api/v1/compliance. get_current_user;
superadmins see all tenants. EU AI Act article posture + Article 73 incident
log + regulator-ready export. See Compliance.
| Method | Path | Purpose |
|---|---|---|
GET | /compliance/status | Per-article (12/13/26/50/73) compliance snapshot |
POST | /compliance/incidents | Log a serious incident (Article 73) |
GET | /compliance/incidents | List incidents (status filter) |
GET | /compliance/export | Regulator-ready JSON over a period (default 180d) |
GET | /compliance/export.pdf | Same data as a downloadable PDF |
Alerts & webhooks
Alerts
routes/alerts.py — prefix /api/v1/alerts. require_user_or_apikey. See
Alerts.
| Method | Path | Purpose |
|---|---|---|
GET | /alerts/rules | List the caller's alert rules |
POST | /alerts/rules | Create a rule (validates the metric) |
PATCH | /alerts/rules/{rule_id} | Update a rule |
DELETE | /alerts/rules/{rule_id} | Delete a rule (cascades events) |
GET | /alerts/events | Recent fired events |
POST | /alerts/test/{rule_id} | Dry-run a rule (compute metric, don't fire) |
GET | /alerts/metrics | Supported metric identifiers |
Webhooks
routes/webhooks.py — prefix /api/v1/webhooks. Reads get_current_user;
mutations + test require require_admin.
| Method | Path | Purpose |
|---|---|---|
GET | /webhooks | List webhook destinations |
POST | /webhooks | Register a destination (admin) |
PATCH | /webhooks/{webhook_id} | Update a destination (admin) |
DELETE | /webhooks/{webhook_id} | Remove a destination (admin) |
POST | /webhooks/{webhook_id}/test | Fire a synchronous test delivery (admin) |
Outbound destinations support Slack, PagerDuty Events v2, and generic HTTPS with HMAC-SHA256 signing. The create/patch/delivery paths enforce an SSRF guard that blocks private/loopback/link-local/cloud-metadata targets.
Identity provider integration (SSO / SCIM)
routes/v21.py — SAML SSO + SCIM provisioning. See SSO & SCIM.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/v1/saml/config/{org_id} | org owner | Create/update the org's SAML IdP config |
GET | /api/v1/saml/config/{org_id} | org admin+ | Read SAML config |
POST | /api/v1/saml/acs/{org_slug} | signed assertion | Assertion Consumer Service (IdP POST → session) |
POST | /api/v1/scim/tokens | org owner | Mint a SCIM bearer token (one-time secret) |
GET | /api/v1/scim/tokens/{org_id} | org admin+ | List SCIM tokens |
POST | /scim/v2/Users | SCIM token | Provision a user |
GET | /scim/v2/Users/{user_id} | SCIM token | Read a provisioned user |
DELETE | /scim/v2/Users/{user_id} | SCIM token | Deprovision (remove membership) |
Pause / resume
routes/pauses.py — prefix /api/v1/pauses. require_user_or_apikey; mutations
12/minute. The Edit + Resume path is gated by
AUDITTRAIL_EXPERIMENTAL_EDIT_RESUME. See Pause / resume.
| Method | Path | Purpose |
|---|---|---|
POST | /pauses | SDK announces a pause checkpoint |
GET | /pauses/{trace_id}/active | Active (or awaiting-edit) pause for a trace, or null |
GET | /pauses/{pause_id} | One pause by id — any status, terminal included (the SDK reads its resumed/expired/abandoned state here once /active returns null) |
POST | /pauses/{pause_id}/heartbeat | SDK keepalive (stale → reaper marks abandoned) |
POST | /pauses/{pause_id}/ack | SDK confirms an edit was applied (awaiting_edit → resumed) |
POST | /pauses/{pause_id}/resume | Operator resume (optionally with an edited state blob) |
Fleet
routes/fleet.py — prefix /api/v1/fleet. require_user_or_apikey. See
Live Fleet.
| Method | Path | Purpose |
|---|---|---|
GET | /fleet/snapshot | Per-agent summary for the caller's recent traces |
GET | /fleet/topology | Aggregated agent → tool topology graph |
Reports
routes/reports.py — prefix /api/v1/reports. get_current_user.
| Method | Path | Purpose |
|---|---|---|
POST | /reports/generate | Generate a PDF audit report (synchronous) |
GET | /reports | List reports (newest first) |
GET | /reports/{report_id} | Report status + metadata |
GET | /reports/{report_id}/download | Download the PDF (409 until ready) |
Analytics
routes/analytics.py — prefix /api/v1/analytics. get_current_user; results
cached 60s.
| Method | Path | Purpose |
|---|---|---|
GET | /analytics/overview | High-level dashboard metric cards |
GET | /analytics/cost | Cost grouped by agent / model / tool |
GET | /analytics/latency | p50/p95/p99 latency by day |
GET | /analytics/tools | Tool usage stats |
GET | /analytics/cost-timeseries | Daily cost totals |
GET | /analytics/tools-timeseries | Daily tool-call counts |
GET | /analytics/slowest-spans | Highest-p99 spans |
GET | /analytics/fleet-why | Fleet-level surrogate feature importances (v3.31.0) |
Fleet WHY (GET /analytics/fleet-why, v3.31.0). Global feature
importances of the tool-selection surrogate — what drives tool selection
across the whole deployment, computed at train time as mean |SHAP|
over the training set (importance_method: "mean_abs_shap"; falls back
to "xgb_gain" when the SHAP explainer is unavailable). The surrogate
is instance-global: it trains on the most recent 500 tool spans across
the deployment, at API startup and automatically in the background once
at least 5 tool spans with two distinct tools exist. Response:
{trained, n_samples, surrogate_f1, trained_at, min_samples_required, importance_method, importances[{name, importance, description}], tool_distribution, user_tool_span_count, reason} — when untrained,
importances is empty and reason states why (no fabricated values).
Settings, health & billing
routes/settings.py + routes/health.py — prefix /api/v1.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /health | none | Service health, DB connectivity, rules loaded, uptime |
GET | /settings | cookie | Effective tenant settings |
GET | /settings/metrics | cookie | Performance metrics for the settings dashboard |
PATCH | /settings | admin | Update tenant settings |
GET | /settings/pricing | cookie | Pricing config: operator overrides, default rate, community-catalog size/freshness (v3.24.0) |
GET | /instance | cookie | Instance metadata (version, agents, rules, uptime) |
routes/v20.py (billing_router) — prefix /api/v1/billing.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /billing/subscriptions/{org_id} | org viewer+ | Subscription state (auto-creates a free row) |
POST | /billing/checkout | org owner | Create a Stripe Checkout session (gated on AUDITTRAIL_STRIPE_SECRET_KEY) |
POST | /billing/webhook | Stripe signature | Stripe webhook (HMAC-SHA256 verified) |
WebSocket
routes/ws.py. The dashboard endpoints authenticate with the session JWT
cookie; the runner endpoint authenticates with an sk-atd-… token. The
canonical paths are under /api/v1/ws/* (the production Caddy only proxies
/api/*); legacy /ws/* paths resolve to the same handlers.
| Path | Auth | Purpose |
|---|---|---|
/api/v1/ws/traces/{trace_id} | session cookie + trace ownership | Subscribe to one trace (replays last 100 events) |
/api/v1/ws/live | session cookie | Subscribe to all of the caller's trace events |
/api/v1/runner/ws | runner token (sk-atd-…) | Local runner daemon link (run / cancel / output / exit) |
Client messages: {"type":"ping"}, {"type":"subscribe","trace_id":"…"},
{"type":"unsubscribe","trace_id":"…"}, and {"type":"command", …} — the
last proposes a DeploymentAction from the live DAG (same tier rules as the
REST path; the WS path only proposes, it never auto-executes Tier 2/3).
Server events use the {"event": <type>, …} envelope (note: event, not
type). Examples: span_start / span_end, trace_complete,
constitutional_violation,
pause_active / pause_resumed / pause.expired / pause.abandoned,
canary.ramped / canary.rolled_back, command.proposed / command.ack,
runner.connected / runner.disconnected / runner.dispatch.output /
runner.dispatch.final.
span_start fires when a running span is ingested and span_end when a
span arrives closed (ok / complete / error) — both HTTP and OTLP
ingest broadcast them, so a streaming SDK run renders spans in the trace
view live, without polling. trace_complete fires once, when the last
running span closes and the trace transitions into a terminal status; its
data carries {status, span_count, total_cost, total_duration_ms}.
Error responses
{ "detail": "Resource not found" }| Status | Meaning |
|---|---|
| 400 | Bad request (validation, invalid cursor, unknown metric/action; gateway unsupported_model when a provider key is set) |
| 401 | Unauthenticated (missing/invalid session, key, or token) |
| 403 | Forbidden (insufficient role; older endpoints use 403 for cross-tenant) |
| 404 | Not found (newer endpoints return 404 for cross-tenant — existence not leaked) |
| 409 | Conflict (duplicate name/email, illegal state transition, already revoked) |
| 410 | Gone (pause TTL expired / abandoned) |
| 412 | Precondition failed (edit-resume requires the feature flag) |
| 415 | Unsupported media type (OTLP requires application/json) |
| 422 | Unprocessable entity (bad request body, bad agent bundle, disallowed runner template) |
| 429 | Too many requests (per-route rate limit) |
| 500 | Internal server error |