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.
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
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/audit/search | Search audit logs |
POST | /api/v1/audit/export | Bounded CSV/JSON export of filtered audit rows |
POST | /api/v1/audit/report | Per-action counts, average latency, and top policies for a window |
GET | /api/v1/audit/session-summary | Session-level activity reporting (Enterprise) |
GET | /api/v1/audit/{id} | Fetch one audit record in full detail |
POST | /api/v1/audit/tool-call | Record a non-LLM tool call |
GET | /api/v1/audit/tenant/{tenant_id} | Fetch tenant audit logs |
POST | /api/v1/audit/summary | Generate 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:
| Header | Notes |
|---|---|
Authorization | Basic auth, validated at the Agent proxy |
X-Tenant-ID | Required 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-Type | JSON 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:
| Field | Notes |
|---|---|
user_email | Optional filter. Partial match (case-insensitive substring) as of v8.7.0 — analyst matches [email protected]. |
client_id | Optional filter. Partial match (case-insensitive substring). |
action | Optional 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_id | Optional filter, exact match on the first-class session_id column — the drill-down from a session-summary bucket into its raw events. |
decision_id | Optional filter, exact match on policy_details->>'decision_id'. |
policy_name | Optional 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_id | Optional filter, exact match on policy_details->>'override_id'. |
start_time | RFC3339 time accepted by the handler |
end_time | RFC3339 time accepted by the handler |
limit | Defaults to 100 when omitted |
offset | Page 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
}
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
| Field | Notes |
|---|---|
id | Audit entry ID |
request_id | Request or workflow correlation ID |
timestamp | Event time |
user_id, user_email, user_role | User context when available |
client_id, tenant_id | Caller and tenant scope |
request_type | Request category like llm_request, workflow_*, or tool_call_audit |
query | Request text as stored (already redacted by the write path) |
policy_decision | Canonical verdicts: allowed, blocked, redacted, needs_approval, error; legacy rows may carry older spellings |
policy_details | Structured policy metadata |
provider, model | Provider details for LLM-backed requests |
response_time_ms, tokens_used, cost | Performance and usage metadata |
redacted_fields | Output fields that were redacted |
error_message | Error detail when present |
response_sample | Truncated sample response |
compliance_flags | Derived compliance metadata |
session_id | AI-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",
"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:
| Field | Notes |
|---|---|
user_email | Case-insensitive partial match |
client_id | Case-insensitive partial match |
action | Canonical verdict, expanded to legacy spellings (same semantics as search) |
session_id | Exact match |
decision_id | Exact match on policy_details->>'decision_id' |
policy_name | Same three-shape match as search |
override_id | Exact match on policy_details->>'override_id' |
start_time, end_time | RFC3339 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:
| Field | Required | Notes |
|---|---|---|
start_time | Yes | RFC3339 |
end_time | Yes | RFC3339, must be after start_time; range capped at 1 year |
user_email | No | Case-insensitive partial match |
action | No | Single 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_nameis required (400when missing); the body is capped at 1 MB- tenant scope comes from
X-Tenant-IDwhen routed through the Agent, otherwise from the Basic-auth client ID; a request with neither returns401 - 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 with403rather than accepting unauthenticated writes - without validated Agent proxy auth, a Basic-auth client may only write into its own tenant scope (
403on mismatch) - attribution is header-sourced, never body-sourced: the Agent forwards the per-developer identity as
X-User-Emailand the AI-tool session asX-Session-Id, which land in the audit row'suser_emailandsession_id - the endpoint honors an
Idempotency-Keyheader: a retry within the TTL returns the originalaudit_idbyte-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_timeandend_timemust be RFC3339end_timemust be afterstart_time- range cannot exceed
1year X-Tenant-IDis required (set by the Agent's auth middleware); missing header returns400. The formerX-Org-IDfallback 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.
Related Docs
- Session Summary API
- Decision & Execution Replay API
- Workflow API
- Auth And Header Matrix
- Community vs Enterprise
Operational Readiness Checklist
Before relying on this page in a production rollout, pair it with the core operations docs:
- Deployment Mode Matrix for self-hosted, Evaluation, Enterprise, SaaS, and In-VPC fit
- Failure Modes And Recovery for degraded-provider, connector, approval, and runtime behavior
- Capacity Planning for sizing and growth signals
- Community vs Evaluation vs Enterprise for limits, support surfaces, and upgrade triggers
