---
title: "Export audit logs (CSV or JSON)"
method: POST
path: "/api/v1/audit/export"
tags: ["Audit"]
---

# Export audit logs (CSV or JSON)

`POST /api/v1/audit/export`

## Request

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

Exports audit logs as a downloadable file. Filters mirror
`/api/v1/audit/search` (same ILIKE user/client matching, canonical
action expansion, JSONB policy filters and date range), so an export
always reconciles with the on-screen search for the same filters.
Tenant scope is forced from the `X-Tenant-ID` header.

The export is capped at **50,000 rows**; when the cap is hit the
response carries the `X-Audit-Export-Truncated: true` and
`X-Audit-Export-Row-Cap` headers so callers can warn that the file is
partial. Free-text CSV cells are formula-escaped (leading `=`, `+`,
`-`, `@`, tab or CR is prefixed with `'`) to neutralize spreadsheet
formula injection.

## Query parameters

- `format` 'csv' | 'json'

## Headers

- `X-Tenant-ID` string, required

## Request body

- object
  - `user_email` string: Case-insensitive partial (ILIKE substring) match
  - `client_id` string: Case-insensitive partial (ILIKE substring) match
  - `action` string: Canonical verdict filter (allowed, blocked, redacted, needs_approval, error); expanded to all historical DB spellings of that verdict.
  - `session_id` string: Exact match on the first-class session_id column
  - `decision_id` string: Matches policy_details->>'decision_id'
  - `policy_name` string: Same three-shape policy_details match as /api/v1/audit/search
  - `override_id` string: Matches policy_details->>'override_id'
  - `start_time` string, date-time
  - `end_time` string, date-time

## Response `200`

Export file (Content-Disposition attachment). CSV columns, in
order: id, timestamp, user_email, tenant_id, org_id,
policy_decision, request_type, query, response_sample, provider,
model, response_time_ms, tokens, correlation_id, session_id.

`response_time_ms` is EMPTY for a row whose writer measured nothing
(#3424), never `0`: a spreadsheet skips an empty cell in AVERAGE()
and would otherwise let every unmeasured row vote that average
towards zero.

- object
  - `entries` AuditLogEntry[]
    - `id` string
    - `request_id` string
    - `timestamp` string, date-time
    - `user_id` integer
    - `user_email` string
    - `user_role` string
    - `client_id` string
    - `tenant_id` string
    - `org_id` string
    - `request_type` string
    - `query` string: The audited query/prompt (already redacted by the write path)
    - `query_hash` string
    - `policy_decision` string: Verdict for the request. Canonical values are allowed, blocked, redacted, needs_approval, error; historical rows may carry legacy spellings.
    - `policy_details` object: Nested decision detail exactly as the writer stored it (policy_ids / reasons / latency_ms plus writer-specific keys like gateway_id, tool_name, decision_id, override_id, policy_matches). Treat keys as writer-specific.
    - `provider` string
    - `model` string: LLM model identifier the request was routed to (e.g. `gpt-4o-mini`, `llama3.2:latest`). Surfaced separately from `provider` so callers can filter audit reads by model without parsing the provider's vendor-specific naming.
    - `response_time_ms` integer: Measured duration for this row in whole milliseconds. ABSENT when this row's writer measured nothing (#3424). Absent is NOT `0`. This field used to be emitted unconditionally, so every row from a writer with no duration to record -- blocked request / response / media, failed request, workflow, plan and tool-call rows, HITL approvals, and the connector-exec MCP closure -- carried a literal `0`, which the portal's Latency column rendered as a confident "0ms". The key is now omitted for those rows rather than set to null: this schema declares no `required` list, so the property was already optional and a conforming client already handles its absence. Nothing about the declared contract changes. A `0` that DOES arrive is a real measurement: the column is whole milliseconds, so a decision the platform timed at under 1ms is stored as the 0 its clock produced. The portal renders that as "<1ms" and an absent value as "-". The quantity differs by `plane`: enforcement duration on decision/gateway/mcp/openai_compat, a PROVIDER round trip on `llm`, and a client-asserted OTLP duration on cowork/claude_code (#3431). The compliance summary's `avg_latency_ms` averages only the first.
    - `tokens_used` integer: Provider tokens consumed by this request, OMITTED when the row carries no RECORDED provider usage (#3427). The omission says the usage was not recorded; it does NOT assert that no provider was called. Two distinct populations omit it: * Rows whose writer never reached a model at all -- a blocked request, a redaction, a gateway pre-check deny, a media deny, a workflow step, a plan or a tool-call row. * Rows written by the LLM RESPONSE-plane block writer, which runs AFTER the forward: the call was made and the answer was withheld, so a real round trip was paid for, but that writer records none of the provider usage it is handed. That is a gap in the writer, tracked separately, not a claim about the request. Until #3427 both populations left the read paths as a literal `0`, which the portal's detail panel rendered as "Tokens 0" under a request no model saw. The key is now omitted for those rows rather than set to null: this schema declares no `required` list, so the property was already optional and a conforming client already handles its absence. A `0` that DOES arrive is a real report from a provider, not an absence.
    - `cost` number, double: Estimated provider cost for this request in USD, OMITTED when the row carries no recorded provider usage (#3427). Same rule, same two populations and same rationale as `tokens_used` above; note that `0` is a genuinely representable cost (a locally hosted or free-tier model), which is why absence is expressed by omitting the key rather than by a zero.
    - `redacted_fields` string[]
    - `error_message` string: Omitted when empty
    - `response_sample` string
    - `compliance_flags` string[]
    - `security_metrics` object
    - `decision_id` string: Canonical decision-row id (#2597/ADR-058); present on planes that mint decisions, omitted on legacy writers
    - `plane` string: Enforcement plane that wrote the row (e.g. mcp, llm); omitted on legacy writers. The orchestrator's four ENFORCEMENT planes are `orchestrator_request` for /api/v1/process and /api/v1/plan/execute, `wcp` for the workflow step gate, `map` for a multi-agent step and `orchestrator_response` for the LLM response. (`policy_simulation` and `policy_test` are the orchestrator's too, but they are operator tools rather than enforcement points and write no decision row.)
    - `correlation_id` string: Decision-chain correlation key (#2611); omitted when not stitched
    - `transfer_basis` string: UU PDP Pasal 56 cross-border transfer legal basis (adequacy, safeguards, pasal_56b_dpa, consent) — Enterprise LLM-forward path only (#2718)
    - `data_residency` string: ISO 3166-1 alpha-2 destination country (#2718); Enterprise only
    - `session_id` string: AI-tool session id (Claude Code / Desktop) forwarded via X-Session-Id; asserted attribution, not an auth boundary
  - `count` integer: Number of entries in this export
  - `truncated` boolean: True when the row cap was hit
  - `row_cap` integer

## Other responses

- `400`: Invalid format value or malformed request body
- `401`: Missing X-Tenant-ID header (tenant scoping required)
- `500`: Audit export 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)
