---
title: "Get a single audit entry by ID"
method: GET
path: "/api/v1/audit/{id}"
tags: ["Audit"]
---

# Get a single audit entry by ID

`GET /api/v1/audit/{id}`

## Request

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

Returns the full audit entry for the given id, tenant-scoped by the
`X-Tenant-ID` header. A record that exists but belongs to another
tenant returns 404 (not 403), so the endpoint cannot be used as a
cross-tenant existence oracle. Literal `/api/v1/audit/*` routes
(search, export, report, session-summary, tenant, tool-call) are
matched before this parameterized path — including `GET
/api/v1/audit/search`, which answers **405** rather than being
swallowed here as an id of `"search"` (#3060).

**Role-scoped reads (#2922):** a non-tenant-wide caller may fetch only
their own rows; a record belonging to another user returns the same
404 as a missing one (non-oracle). See
`POST /api/v1/audit/search` for the deployment-mode carve-outs.

## Path parameters

- `id` string, required

## Headers

- `X-Tenant-ID` string, required

## Response `200`

Full audit entry

- AuditLogEntry: A single audit_logs row as serialized on the wire (orchestrator AuditEntry struct). Optional canonical-decision, cross-border and session fields are omitted when empty.
  - `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

## Other responses

- `400`: Missing audit id
- `401`: Missing X-Tenant-ID header (tenant scoping required)
- `404`: Audit record not found (or belongs to another tenant, or is outside the caller's read scope). Deliberately the same body in all three cases so the endpoint is not an existence oracle — `X-Axonflow-Read-Scope` is the operator-side channel that tells them apart.
- `500`: Audit detail lookup 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)
