---
title: "Per-action audit report"
method: POST
path: "/api/v1/audit/report"
tags: ["Audit"]
---

# Per-action audit report

`POST /api/v1/audit/report`

## Request

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

Aggregates audit logs for a window into per-action counts, average
latency, and top policies, tenant-scoped and optionally filtered by
user and a single canonical action. Counts reconcile with
`/api/v1/audit/search` for the same filters.

## Headers

- `X-Tenant-ID` string, required

## Request body

- object
  - `start_time` string, date-time, required: Window start (RFC3339)
  - `end_time` string, date-time, required: Window end (RFC3339, must be after start_time; range at most 1 year)
  - `user_email` string: Optional case-insensitive partial match
  - `action` string: Optional single canonical action filter

## Response `200`

Per-action report. A fail-closed read (`X-Axonflow-Read-Scope:
none`) returns the seeded all-zeroes report, which has the same
shape as "no rows in the window".

- AuditActionReport: Per-action aggregate for a window. by_action always carries the full canonical verdict set (allowed / blocked / redacted / needs_approval / error) — a verdict with no rows reports 0 rather than being absent.
  - `tenant_id` string
  - `user_email` string: Echoed filter; omitted when not filtered
  - `start_time` string, date-time
  - `end_time` string, date-time
  - `total` integer: Governed decisions in the window (lifecycle events excluded)
  - `by_action` object: Counts folded onto the canonical verdicts
  - `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). `null` is not `0`. Whole planes record no enforcement duration by design (HITL approvals, workflow lifecycle rows, the connector-exec MCP closure), and provider round trips on the LLM plane are a different quantity and are excluded rather than averaged in, so a range whose traffic was entirely one of those has nothing to report. It used to be coalesced to 0, which reads exactly like a measured zero. Clients MUST accept null here; a strict deserializer binding this to a non-optional float will raise. It can also be BELOW 1: response_time_ms is whole milliseconds, so a decision completing in under a millisecond contributes a 0. Read latency_sample_count to tell "fast" from "empty".
  - `latency_sample_count` integer: How many rows backed avg_latency_ms. 0 exactly when avg_latency_ms is null. Never greater than `total`: both are narrowed to the same verdict rows.
  - `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 `total` and are not a share of it. Compare the array length against `total_policies` before presenting it as the complete set. There is no `top_policies_unavailable` flag on this endpoint, unlike the compliance summary: this is the regulator-facing artifact, so a failed or timed-out aggregation fails the WHOLE response with a 500 rather than returning a table that quietly omits rows.
    - `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; 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. On this REGULATOR-FACING report an undisclosed truncation is the sharper of the two surfaces' risks: when this exceeds the array length, disclose "top 10 of N" rather than implying the remainder does not exist.

## Other responses

- `400`: Invalid body, non-RFC3339 timestamps, end_time not after start_time, or range over 1 year
- `401`: Missing X-Tenant-ID header (tenant scoping required)
- `500`: Audit report failed
- `503`: Audit subsystem unavailable

---

[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)
