Session-Summary API (Enterprise)
GET /api/v1/audit/session-summary rolls the governed audit trail up into per-session (or, for records that carry no session, per-user-per-day) buckets: request totals, an allow / block / redact verdict breakdown, a per-request-type usage split, and token / cost / latency aggregates. When the OTLP metrics ingest is wired, each bucket is additively enriched with the Claude Code usage counters (lines of code, active time, commits, tool-permission decisions, and the CLI's own token / cost aggregates).
It is the reporting view over the same audit_logs store every governance plane writes to — see Audit Logging for what lands there. The endpoint returns aggregates only, never raw event content; to drill into a bucket's individual records, use the audit search API (below). It answers questions like "what did this developer's sessions look like this week — how much activity, how much was blocked, what did it cost?" without paging through individual audit rows.
Session-summary reporting is an Enterprise capability, available from platform v9.6.0 (see the v9.6.0 release notes). In Community the route exists but returns 501 Not Implemented — a clear signal rather than a 404.
API reference
GET /api/v1/audit/session-summary?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD[&user_email=...][&limit=N]
Served by the orchestrator, alongside the other audit read endpoints (/api/v1/audit/search, /export, /report).
Authentication and tenant scoping
The tenant scope comes from the X-Tenant-ID request header — the same trusted-header model as the rest of the audit read API. A request without it is rejected with 401. Every query the endpoint runs is scoped to that tenant; there is no query-string tenant parameter (a caller-supplied one would be a cross-tenant leak, so it does not exist).
Because the header is trusted, this API — like the whole orchestrator audit surface — belongs behind your trust boundary: in a standard deployment it is fronted by the customer portal or an internal gateway that sets the header from an authenticated identity. Do not expose the orchestrator's audit routes directly to callers you would not trust to name their own tenant.
Query parameters
| Parameter | Required | Description |
|---|---|---|
start_date | yes | First calendar day of the window, YYYY-MM-DD. |
end_date | yes | Last calendar day of the window, YYYY-MM-DD, inclusive — the window runs through the end of this day. Must be on or after start_date. |
user_email | no | Filter to matching users. Case-insensitive substring match (ILIKE), so alice matches [email protected]. |
limit | no | Maximum number of buckets returned. Default 200, server maximum 1000 (a larger value is clamped, and the effective bound is echoed back as bucket_limit). A non-integer or non-positive value is a 400, not silently ignored. |
Additional request rules:
- The window may span at most 1 year; a longer range is a
400. - The window is additionally floored to the tenant's retention window — the same tier-based floor
/api/v1/audit/searchand/exportapply. Asking for dates older than retention silently returns only what is retained. - Errors:
400(bad dates / bad limit),401(missingX-Tenant-ID),500(query failure),503(audit subsystem unavailable),501(Community edition).
How records are bucketed
- A record that carries a
session_id(Claude Code / Cowork OTEL events, and any plane that propagates a session) is grouped into that session's bucket. The bucket'ssession_idfield is set anddayis absent. - A record with no session falls back to one bucket per user per calendar day. The bucket's
dayfield (YYYY-MM-DD) is set andsession_idis absent. user_emailis always part of the bucket key, so two developers' session-less activity on the same day never collapses into one bucket.- Buckets are ordered most-recent activity first (by each bucket's latest record), with deterministic tie-breaking — the same window returns the same order on every call.
- Non-verdict lifecycle records (policy-override grants / revocations) are excluded from every aggregate, so
by_actionalways sums tototaland latency / token numbers are never dragged by rows that carried no governed call.
Response schema
The envelope:
| Field | Type | Description |
|---|---|---|
tenant_id | string | The tenant the report is scoped to (from X-Tenant-ID). |
user_email | string | Echo of the user_email filter; omitted when not supplied. |
start_date / end_date | string | Echo of the requested window. |
buckets | array | The session / day buckets, most-recent activity first. |
bucket_limit | int | The effective bucket cap applied to this response. |
truncated | bool | true when the window held more buckets than the cap — narrow the window, filter by user, or raise limit (up to the server max). |
Each entry in buckets:
| Field | Type | Description |
|---|---|---|
session_id | string | Set for a real-session bucket; absent on day-fallback buckets. |
day | string | YYYY-MM-DD; set only for day-fallback buckets. |
user_email | string | The user the bucket belongs to. |
tenant_id | string | Always the scoped tenant. |
start_time / end_time | timestamp | First and last record in the bucket (RFC 3339). |
total | int | Number of governed records in the bucket. |
by_action | object | Per-verdict counts. Always contains all five canonical verdicts — allowed, blocked, redacted, needs_approval, error — seeded to 0, so the keys sum to total. |
tools | array | Per-request_type usage split (see the note below): {request_type, count, tokens_used, cost, avg_latency_ms}. |
tokens_used / cost / avg_latency_ms | int / number / number | Bucket-level aggregates over the governed records. |
usage_metrics | object | Optional Claude Code usage enrichment — absent unless matching usage metrics exist (see Usage-metric enrichment). |
request_type is the call type, not the tool nameThe tools array breaks a bucket down by the audit record's request_type — the call-type discriminator (llm_call, mcp_check_policy, claude_code_tool_result, claude_code_api_request, …). It is not Claude's per-tool name (Edit, Bash, a specific MCP tool): finer-grained tool identity lives in different fields per plane and is not aggregated here. Where a tool name is recorded at all, it is on the individual records (see the fidelity notes below) — drill down with the audit search API to see them.
Example
curl -s "https://<your-axonflow-orchestrator>/api/v1/audit/session-summary?start_date=2026-07-01&end_date=2026-07-07&user_email=alice&limit=50" \
-H "X-Tenant-ID: acme-corp"
{
"tenant_id": "acme-corp",
"user_email": "alice",
"start_date": "2026-07-01",
"end_date": "2026-07-07",
"buckets": [
{
"session_id": "0d3adb18-2f6c-4e0a-9d5f-6c1f2b8a4e70",
"user_email": "[email protected]",
"tenant_id": "acme-corp",
"start_time": "2026-07-06T09:14:02Z",
"end_time": "2026-07-06T11:47:55Z",
"total": 183,
"by_action": {
"allowed": 169,
"blocked": 3,
"redacted": 11,
"needs_approval": 0,
"error": 0
},
"tools": [
{ "request_type": "claude_code_api_request", "count": 57, "tokens_used": 88410, "cost": 1.7302, "avg_latency_ms": 2310.4 },
{ "request_type": "claude_code_tool_result", "count": 63, "tokens_used": 0, "cost": 0, "avg_latency_ms": 0 },
{ "request_type": "claude_code_user_prompt", "count": 19, "tokens_used": 0, "cost": 0, "avg_latency_ms": 0 },
{ "request_type": "claude_code_tool_decision", "count": 44, "tokens_used": 0, "cost": 0, "avg_latency_ms": 0 }
],
"tokens_used": 88410,
"cost": 1.7302,
"avg_latency_ms": 2310.4,
"usage_metrics": {
"lines_of_code": 412,
"active_time_seconds": 5820.5,
"commits": 3,
"pull_requests": 1,
"tool_permission_decisions": { "accept": 41, "reject": 3 },
"session_count": 1,
"tokens_used": 91266,
"cache_tokens": 5104233,
"cost_usd": 1.83
}
},
{
"day": "2026-07-03",
"user_email": "[email protected]",
"tenant_id": "acme-corp",
"start_time": "2026-07-03T08:02:11Z",
"end_time": "2026-07-03T17:31:40Z",
"total": 36,
"by_action": { "allowed": 34, "blocked": 0, "redacted": 2, "needs_approval": 0, "error": 0 },
"tools": [
{ "request_type": "llm_call", "count": 36, "tokens_used": 21400, "cost": 0.42, "avg_latency_ms": 1810.2 }
],
"tokens_used": 21400,
"cost": 0.42,
"avg_latency_ms": 1810.2
}
],
"bucket_limit": 50,
"truncated": false
}
The second bucket is a day-fallback bucket (SDK traffic that carried no session id) — note day in place of session_id, and no usage_metrics (none of the usage counters matched it). Latency averages count only the records that carry a latency (LLM / API calls); event-only records (prompts, tool results) do not drag them toward zero.
Drilling into a bucket
The summary never contains raw event content. To page through a bucket's underlying records, use POST /api/v1/audit/search (same trusted-header scoping):
- Session bucket: filter by
session_id—{"session_id": "0d3adb18-…", "limit": 100}. Thesession_idsearch filter ships in v9.6.1 (see the v9.6.1 release notes); on v9.6.0 itself the search API ignores it — use the bucket's user and time bounds (below) instead. - Day-fallback bucket: filter by the user and the bucket's time bounds —
{"user_email": "[email protected]", "start_time": "…", "end_time": "…"}.
user_email is a substring match on the search API too — pass the full address, or the drill-down can return a lookalike's rows (alice@ also matches malice@) and stop reconciling with the bucket. See Audit Logging for the search API and record shape.
Usage-metric enrichment
The usage_metrics block carries Claude Code's own exported usage counters, aggregated onto the same bucket:
| Field | Description |
|---|---|
lines_of_code | Lines of code added + removed. |
active_time_seconds | Active usage time. |
commits / pull_requests | Git commits / PRs created. |
tool_permission_decisions | {accept, reject} split of Claude Code's local edit-tool permission prompts. |
session_count | CLI sessions started. |
tokens_used | The CLI's exported token total — input + output only. |
cache_tokens | Cache-read + cache-creation tokens, kept separate (on a real session they run orders of magnitude larger than input/output and would swamp the headline number). |
cost_usd | The CLI's exported cost total. |
It appears only when both are true: the deployment ingests Claude Code's OTLP metrics export (/v1/metrics — see Claude Cowork & Claude Code OTEL Ingest for setup, and the Metrics Reference for the stored counters), and matching usage records exist for the bucket. Otherwise the field is simply absent and the base view above still works from the audit trail alone.
The enrichment is deliberately conservative:
- It never creates buckets. Buckets are defined by governed audit records; a session that exported usage metrics but produced no governed activity does not appear.
- It never alters the base view. If the usage lookup fails, the response degrades to the un-enriched buckets rather than erroring.
- Two token numbers, two meanings. The bucket-level
tokens_used/costare sums over governed audit records — authoritative for what flowed through AxonFlow.usage_metrics.tokens_used/cost_usdare the CLI's own export, which counts everything the tool did, governed or not. They are nested and named differently so the two are never confused; expect the CLI numbers to run somewhat higher.
Building a session report on AxonFlow signal
A common ask: reproduce a per-session "session note" — Summary, what was asked, what happened, files touched, commands run, approvals, token usage — from governed data. AxonFlow deliberately exposes the signal for that report rather than generating the narrative itself; the generation step (and where the generated notes are stored) stays on your side, under your own data-handling rules. Here is where each report field comes from:
| Report field | Source | Content fidelity |
|---|---|---|
| Token usage, cost, latency | This API — bucket aggregates + usage_metrics | Exact numbers. |
| Activity volume, allow / block / redact outcomes | This API — total, by_action, tools | Exact counts. |
| Active time, lines of code, commits, PRs | This API — usage_metrics | Exact counters (when the metrics ingest is wired). |
| Approvals / rejections | This API — usage_metrics.tool_permission_decisions; per-event detail via audit search (tool_decision records) | Exact. |
| What was asked | Audit search over the bucket — user_prompt records | Redacted prompt text (see below); requires prompt capture enabled at the client. |
| Files touched, commands run | Audit search over the bucket — tool_result records, whose content carries the tool input (file path, command line) | Redacted tool-input text; requires tool-detail capture enabled at the client. With capture off, the record instead keeps a fixed descriptor naming the tool. |
| What happened (narrative) | A generation step you run over the two rows above | Yours — AxonFlow supplies the inputs, not the prose. |
Masked vs. verbatim
Two properties of the underlying store shape what a report can quote:
- Claude Code / Cowork OTEL events (
user_prompt,tool_result,assistant_response) store their content PII-masked, redacted before persistence, fail-closed — if the redaction engine is unavailable the content is withheld rather than stored in the clear. Content capture itself is opt-in at the client (OTEL_LOG_USER_PROMPTS,OTEL_LOG_TOOL_DETAILS): with capture off, the event still lands as an audit record with a fixed placeholder descriptor and its structural metadata (session id, model, tokens, cost — and, for tool and approval events, the tool name). With tool-detail capture on, atool_resultrecord's content is the redacted tool input itself; the tool name then appears structurally only on the approval (tool_decision) records. See the OTEL ingest page for the full capture and redaction contract. - MCP governance records (the proxy / connector plane) store tool parameters as a hash, not text — that plane proves what was called and decided, not the argument values.
So a governed report can say which files and commands, with PII masked, when OTEL tool-detail capture is on — but it is not a verbatim transcript, by design. That is the honest trade: the same masking that makes the store safe to centralize means quoted text is the redacted form.
When verbatim is required (e.g. an engineering-facing note that must quote the exact command), use a client-side join: Claude Code keeps its own session transcripts locally on the developer's machine, at full fidelity, identified by the same session id the telemetry carries. Join it to the governed record at generation time, on the client, and treat the result under the stricter of the two sensitivity rules — the governed store stays masked, and the verbatim text never transits AxonFlow.
Attribution for all of the above is per-developer via user_email — see Per-Developer Identity for when a real email is present on the telemetry and how to pin it for a fleet.
Related
- Metrics Reference — the stored Claude Code usage counters this API's enrichment reads, and every other metric AxonFlow emits.
- Grafana Dashboard — the operator-side Claude Code Usage dashboard over the same usage store (cross-org; this API is the tenant-scoped surface).
- Claude Cowork & Claude Code OTEL Ingest — how session-keyed audit records and usage metrics land in the first place.
- Audit Logging — the canonical store, and the search API used to drill into a bucket.
- Per-Developer Identity — attributing sessions to individual developers.
- v9.6.0 Release Notes — where this API shipped.
