Skip to main content

Audit API

Use the Audit API to search and export request history, generate compliance summaries and action reports, record non-LLM tool calls, and inspect tenant-scoped audit trails. This is one of the most important surfaces for regulated AI systems because it turns governance decisions into something security, compliance, and platform teams can actually review.

Generated API reference

Full request bodies, response schemas, and status codes for every endpoint on this page are in the generated Orchestrator API reference, rendered from the published OpenAPI spec. Use the links in the table below to jump straight to an operation.

Endpoints

MethodPathPurpose
POST/api/v1/audit/searchSearch audit logs
POST/api/v1/audit/exportBounded CSV/JSON export of filtered audit rows
POST/api/v1/audit/reportPer-action counts, average latency, and top policies for a window
GET/api/v1/audit/session-summarySession-level activity reporting (Enterprise)
GET/api/v1/audit/{id}Fetch one audit record in full detail
POST/api/v1/audit/tool-callRecord a non-LLM tool call
GET/api/v1/audit/tenant/{tenant_id}Fetch tenant audit logs
POST/api/v1/audit/summaryGenerate a compliance summary

Base URL, recommended:

http://localhost:8080

The Agent commonly proxies these requests to the Orchestrator. For hardened deployments, the tool-call write path is especially important to send through the Agent because the Orchestrator may require the internal proxy-auth header it injects.

Authentication

Common headers:

HeaderNotes
AuthorizationBasic auth, validated at the Agent proxy
X-Tenant-IDRequired by every audit read endpoint (search, export, report, session-summary, {id}, tenant/{tenant_id}, summary). Injected by the Agent proxy from the authenticated client — client-supplied values are overwritten — so only direct-to-Orchestrator callers set it themselves. Missing header → 401 (or 400 on summary).
Content-TypeJSON POST bodies

Tenant scoping is always forced server-side from X-Tenant-ID; none of these endpoints accept a client-controlled tenant_id filter in the request body, and X-Org-ID is not accepted as a tenant fallback on the audit surface. See the Auth And Header Matrix for how the Agent derives these headers from Basic auth.

Search Audit Logs

POST /api/v1/audit/search (searchAuditLogs) accepts a JSON body with these verified fields:

FieldNotes
user_emailOptional filter. Partial match (case-insensitive substring) as of v8.7.0 — analyst matches [email protected].
client_idOptional filter. Partial match (case-insensitive substring).
actionOptional filter on policy_decision. Canonical values: allowed, blocked, redacted, needs_approval, error. The value is normalized (legacy spellings like deny map to blocked) and expanded to every historical DB spelling of that verdict, so the filter also matches pre-canonicalization rows.
session_idOptional filter, exact match on the first-class session_id column — the drill-down from a session-summary bucket into its raw events.
decision_idOptional filter, exact match on policy_details->>'decision_id'.
policy_nameOptional filter. Matches any of the three shapes audit writers use: scalar policy_details->>'policy_name', substring of policy_details->>'policy_names', or nested policy_matches[*].policy_name.
override_idOptional filter, exact match on policy_details->>'override_id'.
start_timeRFC3339 time accepted by the handler
end_timeRFC3339 time accepted by the handler
limitDefaults to 100 when omitted
offsetPage offset, applied to the result window. Default 0.

Tenant scope is not a body field: it is forced from the X-Tenant-ID header (missing header → 401), and any tenant_id in the JSON body is ignored. A body must be present (send {} for an unfiltered search); a malformed body returns 400. The tenant's tier-based audit-retention floor clamps start_time — a search cannot reach past the retention window even with an earlier start_time.

The response shape is verified in code as:

{
"entries": [],
"total": 0,
"limit": 100,
"offset": 0
}
v8.7.0 audit-feed corrections

Three behaviors were corrected in v8.7.0 so the audit feed paginates and reports correctly: total is now the true count of matching rows, not the size of the returned page (so a UI can render an accurate "1–100 of 4,213"); offset is now actually applied to the query (page 2 no longer repeats page 1); and the user_email / client_id filters do case-insensitive partial matching so an operator can search by a fragment. An action filter on policy_decision was also added, later hardened to the canonical verdict vocabulary above.

Recording Tool Calls

FieldNotes
idAudit entry ID
request_idRequest or workflow correlation ID
timestampEvent time
user_id, user_email, user_roleUser context when available
client_id, tenant_idCaller and tenant scope
request_typeRequest category like llm_request, workflow_*, or tool_call_audit
queryRequest text as stored (already redacted by the write path)
policy_decisionCanonical verdicts: allowed, blocked, redacted, needs_approval, error; legacy rows may carry older spellings
policy_detailsStructured policy metadata
provider, modelProvider details for LLM-backed requests
response_time_ms, tokens_used, costPerformance and usage metadata
redacted_fieldsOutput fields that were redacted
error_messageError detail when present
response_sampleTruncated sample response
compliance_flagsDerived compliance metadata
session_idAI-tool session ID (Claude Code / Desktop) forwarded via X-Session-Id. Empty string on rows written before the column existed or by writers that carry no session.

Audit rows also carry first-class plane, correlation_id, and decision_id columns (empty on pre-migration rows). Search responses do not include them; fetch the single-record detail endpoint to read them, or use the export endpoint, which includes correlation_id and session_id. Search and detail payloads additionally serialize two vestigial fields — query_hash (always "") and security_metrics (always null) — which the read paths never populate; ignore them.

Example:

{
"entries": [
{
"id": "aud_001",
"request_id": "req_abc123",
"timestamp": "2026-03-29T11:58:00Z",
"user_email": "[email protected]",
"client_id": "security-app",
"tenant_id": "tenant-123",
"request_type": "llm_request",
"query": "Summarize the quarterly security report",
"policy_decision": "allowed",
"policy_details": {
"applied_policies": ["tenant-cost-review"],
"risk_score": 0.12
},
"provider": "openai",
"model": "gpt-4o",
"response_time_ms": 842,
"tokens_used": 598,
"cost": 0.0049,
"redacted_fields": [],
"response_sample": "The quarterly security report highlights...",
"compliance_flags": [],
"session_id": "sess_9f2c"
}
],
"total": 1,
"limit": 100,
"offset": 0
}

This is the endpoint staff engineers usually wire into internal admin consoles, SIEM forwarders, and incident triage tools.

Export Audit Logs

POST /api/v1/audit/export?format=csv|json produces a downloadable, bounded export of filtered audit rows. format defaults to json; anything other than csv or json returns 400.

The JSON body carries the same filters the search endpoint honors, so an export always reconciles with the on-screen search for the same filters:

FieldNotes
user_emailCase-insensitive partial match
client_idCase-insensitive partial match
actionCanonical verdict, expanded to legacy spellings (same semantics as search)
session_idExact match
decision_idExact match on policy_details->>'decision_id'
policy_nameSame three-shape match as search
override_idExact match on policy_details->>'override_id'
start_time, end_timeRFC3339 window

The body is optional: an empty body produces an unfiltered export within the tenant and retention window. A present-but-malformed body returns 400. Tenant scope is forced from X-Tenant-ID (missing → 401), and the tier-based retention floor clamps start_time exactly as on search.

Row cap and truncation headers. A single export returns at most 50,000 rows, newest first. When the filter matches more rows than the cap, the response carries:

X-Audit-Export-Truncated: true
X-Audit-Export-Row-Cap: 50000

JSON format (Content-Disposition: attachment; filename="audit-export-<timestamp>.json"):

{
"entries": [],
"count": 0,
"truncated": false,
"row_cap": 50000
}

Exported entries include org_id, correlation_id, and session_id in addition to the search-response fields (plane and decision_id are only on the detail endpoint).

CSV format (Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="audit-export-<timestamp>.csv") uses this fixed column order:

id, timestamp, user_email, tenant_id, org_id, policy_decision, request_type,
query, response_sample, provider, model, response_time_ms, correlation_id, session_id

Free-text cells are neutralized against spreadsheet formula injection: a cell starting with =, +, -, @, tab, or CR is prefixed with a single quote so Excel/Sheets render it as literal text.

Other statuses: 503 when the audit subsystem is unavailable, 500 on query failure.

Audit Record Detail

GET /api/v1/audit/{id} returns one audit record in full, scoped to the caller's tenant. The record includes everything the search entries carry plus org_id, correlation_id, decision_id, and plane (the enforcement plane that wrote the row; empty on rows written before these columns existed).

Statuses: 200 with the record; 401 when X-Tenant-ID is missing; 404 when no record matches the ID within the caller's tenant (deliberately indistinguishable from "belongs to another tenant"); 503 when the audit subsystem is unavailable; 500 on query failure.

Action Report

POST /api/v1/audit/report aggregates a time window into per-verdict counts, average latency, and top policies — the shape behind report views that need counts which reconcile with search results for the same filters.

Request body:

FieldRequiredNotes
start_timeYesRFC3339
end_timeYesRFC3339, must be after start_time; range capped at 1 year
user_emailNoCase-insensitive partial match
actionNoSingle canonical verdict filter

Response (200):

{
"tenant_id": "tenant-123",
"start_time": "2026-07-01T00:00:00Z",
"end_time": "2026-07-08T00:00:00Z",
"total": 1200,
"by_action": {
"allowed": 1092,
"blocked": 12,
"redacted": 90,
"needs_approval": 4,
"error": 2
},
"avg_latency_ms": 142.3,
"top_policies": []
}

by_action always carries the full canonical verdict set — a verdict with no rows reports 0 rather than being absent. Non-verdict lifecycle rows (override grant/revoke events) are excluded from total, by_action, and the latency average. top_policies is the same policy_name / trigger_count / block_count array as the compliance summary, limited to the top 10 by trigger count.

Statuses: 401 missing X-Tenant-ID; 400 for a malformed body, non-RFC3339 times, end_time not after start_time, or a range over 1 year; 503 audit subsystem unavailable; 500 on query failure.

Session Summary

GET /api/v1/audit/session-summary (Enterprise) returns deterministic per-session activity buckets over the audit trail — verdict counts, per-request-type usage, tokens/cost — grouped by session_id where rows carry one, with a per-user per-day fallback for sessionless rows. On community builds the route exists but returns 501. Drill into any bucket's raw events with POST /api/v1/audit/search and its session_id filter.

This page does not duplicate the full contract — see the Session Summary API reference for parameters, response shape, and usage-metric enrichment.

Record Tool Calls

POST /api/v1/audit/tool-call (auditToolCall) is for non-LLM actions such as MCP executions, third-party API calls, or framework-level function invocations.

Verified required behavior:

  • tool_name is required (400 when missing); the body is capped at 1 MB
  • tenant scope comes from X-Tenant-ID when routed through the Agent, otherwise from the Basic-auth client ID; a request with neither returns 401
  • when internal-service auth is configured, direct (non-Agent) requests are rejected with 403 — in any non-community deployment where it is not configured, the handler fails closed with 403 rather than accepting unauthenticated writes
  • without validated Agent proxy auth, a Basic-auth client may only write into its own tenant scope (403 on mismatch)
  • attribution is header-sourced, never body-sourced: the Agent forwards the per-developer identity as X-User-Email and the AI-tool session as X-Session-Id, which land in the audit row's user_email and session_id
  • the endpoint honors an Idempotency-Key header: a retry within the TTL returns the original audit_id byte-for-byte instead of writing a duplicate row
curl -X POST http://localhost:8080/api/v1/audit/tool-call \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-d '{
"tool_name": "weather-api",
"tool_type": "api",
"input": {"city": "San Francisco"},
"output": {"temperature": 18},
"duration_ms": 245,
"success": true
}'

Successful responses return HTTP 201 Created:

{
"audit_id": "aud_tc_abc123",
"status": "recorded",
"timestamp": "2025-01-02T14:30:00Z"
}

The tool-call handler stores your request in policy_details, so fields like tool_type (see below), input, output, workflow_id, step_id, duration_ms, policies_applied, success, and error_message are preserved for later search and summary flows.

Platform v9.11.0 adds caller_name (v9.11.0+) — the calling client or integration (for example claude_code, codex, cursor, or openclaw) — as the successor to tool_type. On v9.11.0 and later, the audit/tool-call handler folds a supplied tool_type into caller_name when caller_name is omitted, stops storing tool_type as a separate policy_details field, and records the default "unknown" when neither is supplied — an unidentified caller is no longer silently attributed to a specific client. On platforms below v9.11.0, caller_name is accepted but ignored and tool_type is the field that is stored — so keep sending tool_type for attribution there, and switch to caller_name on v9.11.0+.

Tenant Audit Log Reads

GET /api/v1/audit/tenant/{tenant_id} (getTenantAuditLogs) returns the same entries, total, limit, and offset wrapper used by search, but scoped directly to one tenant ID from the path. The page_size query parameter is a deprecated alias for limit, retained for compatibility.

The path tenant is cross-checked against the session tenant: a missing X-Tenant-ID header returns 401, and a path {tenant_id} that does not match the header returns 403 — a caller can never read another tenant's logs by editing the URL. The tier-based retention window applies, and on this endpoint total is the size of the returned page (not the true matching-row count that search reports); offset is always 0.

Compliance Summary

POST /api/v1/audit/summary (getAuditSummary) is the fastest way to build compliance dashboards and review windows for internal controls.

Verified request rules:

  • start_time and end_time must be RFC3339
  • end_time must be after start_time
  • range cannot exceed 1 year
  • X-Tenant-ID is required (set by the Agent's auth middleware); missing header returns 400. The former X-Org-ID fallback was removed in v7.0.0.

Verified response shape:

{
"total_requests": 1200,
"allowed_requests": 1092,
"blocked_requests": 12,
"modified_requests": 90,
"needs_approval_requests": 4,
"error_requests": 2,
"block_rate_percent": 1.0,
"avg_latency_ms": 142.3,

"total_events": 1200,
"by_severity": {
"critical": 12,
"warning": 92,
"info": 1096
},
"by_action": {
"llm_request": 900,
"mcp_query": 150,
"workflow_execution": 100,
"workflow_step_gate": 50
},
"top_policies": [],
"compliance_score": 99.0
}

The card-view aggregates (total_requests, allowed_requests, blocked_requests, modified_requests, block_rate_percent, avg_latency_ms) were added in v7.4.1; needs_approval_requests and error_requests were split out later when verdict triage moved to the canonical vocabulary. The portal Compliance Summary card reads these directly. Every row's policy_decision is normalized to a canonical verdict and each verdict gets its own bucket, so the arithmetic always closes: total_requests == allowed + blocked + modified + needs_approval + error (modified counts redacted verdicts). Needs-approval and error rows are not swept into Allowed, and an unrecognized decision value fails safe to error — never to allowed. Override grant/revoke lifecycle rows are not verdicts: they count in total_events / by_action but are excluded from total_requests, block_rate_percent, and compliance_score, so a burst of override events can't move a compliance metric.

block_rate_percent is blocked / total_requests; compliance_score is (total_requests - blocked) / total_requests, as percentages. avg_latency_ms is a separate query over response_time_ms excluding rows where latency wasn't measured (HITL decisions, workflow-lifecycle events).

Legacy fields (total_events, by_severity, by_action, top_policies, compliance_score) are retained for back-compat. by_action is keyed by request_type; by_severity maps blocked → critical, redacted and error → warning, allowed and needs-approval → info. top_policies is an array of objects with policy_name, trigger_count, and block_count, which makes this route especially useful for compliance dashboards and procurement review material.

Operational Readiness Checklist

Before relying on this page in a production rollout, pair it with the core operations docs: