---
title: "Get audit compliance summary"
method: POST
path: "/api/v1/audit/summary"
tags: ["Audit"]
---

# Get audit compliance summary

`POST /api/v1/audit/summary`

## Request

- Base URL: `https://orchestrator.getaxonflow.com`
- URL: `https://orchestrator.getaxonflow.com/api/v1/audit/summary`
- Auth: none declared. Required headers: `X-Tenant-ID`
- TypeScript: <https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api/docs/api/v1/audit/summary/post.ts> (a function with typed inputs; its response is checked against the schema)

Returns aggregated compliance summary statistics for a given date range.
Includes total event counts, breakdowns by severity and action type,
top triggered policies, and an overall compliance score.

## Headers

- `X-Tenant-ID` string, required

## Request body

- object
  - `start_time` string, date-time, required: Start of date range (RFC3339)
  - `end_time` string, date-time, required: End of date range (RFC3339, must be after start_time)

## Response `200`

Compliance summary. A fail-closed read (`X-Axonflow-Read-Scope:
none`) returns the zero-events summary, whose `compliance_score`
of 100 reads as "all clear" — the header is what tells the two
apart.

- object
  - `total_events` integer: Total audit events in the date range
  - `by_severity` object: Event counts by severity (info, warning, critical)
  - `by_action` object: Event counts by action type (llm_call, tool_call, etc.)
  - `top_policies` object[]: Top 10 policies by trigger count. A row is counted once per DISTINCT policy it recorded, so these counts do not sum to the number of audit rows and are not a share of one. Compare the array length against `total_policies` before presenting it as the complete set.
    - `policy_name` string: The policy identity resolved for this group by the shared identity chain. IDENTITY-first: it carries a raw policy IDENTIFIER on every row whose writer stamped one, and a display NAME only when it did not. Read `identity_is_name` before rendering; styling an identifier as though it were a display name is exactly the defect #3347 fixed.
    - `identity_is_name` boolean: Whether `policy_name` holds a display NAME (true) or a raw IDENTIFIER (false). It is a property of the RESOLVED STRING, NOT a claim about what the writer recorded. Since #3365 a decide-plane row stamps `policy_names` alongside `policy_ids`, so such a row records a name AND reports false here, because the chain resolves the id arm first. Render a false value with a NEUTRAL identifier affordance (the portal appends "(policy id)"); a "name not recorded" affordance would assert a falsehood about that row.
    - `trigger_count` integer
    - `block_count` integer
  - `total_policies` integer: How many DISTINCT policies fired in the range BEFORE the 10-entry `top_policies` limit truncated it. 0 when nothing fired. When it exceeds the array length, disclose "top 10 of N" rather than implying the remainder does not exist.
  - `top_policies_unavailable` boolean: Present and `true` ONLY when the top-policies aggregation did not complete (it failed, or exceeded its 15s deadline). OMITTED entirely otherwise, so treat absence as false. The rest of the summary is still returned, so without this flag a failed aggregation is INDISTINGUISHABLE from "no policies fired": `top_policies` empty, `total_policies` 0, and any truncation disclosure computing "nothing hidden". Clients MUST NOT render that as a clean zero result. The portal replaces the tile with an explicit "could not be computed for this range" notice. The `/api/v1/audit/report` twin of this field does not exist: on that endpoint a failed aggregation fails the whole response with a 500 instead of truncating.
  - `compliance_score` number, float: Compliance score 0-100 (100 = no blocked events)
  - `avg_latency_ms` number, double, nullable: Mean ENFORCEMENT latency in milliseconds over the rows in the range that carried a measurement, or `null` when none did (#3424). Same contract as the /api/v1/audit/report field of this name: `null` is the absence of a measurement, NOT a measured zero, and clients MUST accept it (a strict deserializer binding this to a non-optional float will raise). Values below 1 are legitimate -- response_time_ms is whole milliseconds, so a decision completing in under one contributes a 0.
  - `latency_sample_count` integer: How many rows backed avg_latency_ms. 0 exactly when avg_latency_ms is null, and never greater than total_requests. Routinely far below it: planes that record no enforcement duration (HITL approvals, workflow lifecycle rows, the connector-exec MCP closure) and the LLM plane's provider round trips are excluded from the average while still being governed rows. The portal renders this as the tile's basis so an average over a small subset cannot read like an average over all of them.

## Other responses

- `400`: Invalid request (bad/missing RFC3339 timestamps, end_time not after start_time, range over 1 year) or missing X-Tenant-ID header.
- `500`: Internal server error

---

[API](https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api.md) - [All operations](https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api/llms.txt) - [OpenAPI document](https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api/revisions/f408ef3ec9dd?raw)
