# Logs API — Query the Agent Audit Trail

REST reference for the ACP audit log. Pull every governed tool call — its decision, the identity behind it, and the delegation chain — with an API key.

# Logs

Every tool call a governed agent makes is recorded: the tool, the decision, the identity behind it, and — for delegated work — the chain it came down. This endpoint reads that trail, so you can reconcile spend, feed a SIEM, or reproduce a governance scorecard from code with no Firebase SDK.

```
GET https://api.agenticcontrolplane.com/<workspace>/admin/audit
```

Requires a key with the `admin.audit.read` scope (or `*`), or an owner/admin session. The workspace slug in the path must match your key.

## Request

```bash
curl -s "$ACP/acme/admin/audit?since=2026-06-25T00:00:00Z&limit=200&tool=github.create_issue" \
  -H "Authorization: Bearer $ACP_KEY"
```

| Query param | Type | Default | Description |
|-------------|------|---------|-------------|
| `since` | ISO-8601 | 15 minutes ago | Return only entries at or after this time. A bad timestamp returns `400`. |
| `until` | ISO-8601 | — | Upper bound on `ts`. Page backwards through a busy window with it. |
| `limit` | number | `200` | Max entries to fetch. Clamped to `1`–`1000`. The filters below apply to the fetched window, so a narrow filter can return fewer than `limit`. |
| `tool` | string | — | Exact tool-name filter (classified or invoked name). |
| `decision` | string | — | The stored decision: `allow`, `deny` (PreToolUse); `pass`, `redact`, `block` (PostToolUse). |
| `outcome` | string | — | The derived outcome, see below: `allow` · `deny` · `held` · `approved` · `expired` · `paused` · `ungoverned` · `flagged` · `pass` · `redact` · `block` · `model`. |
| `agent` | string | — | Exact `agentName`. |
| `sub` | string | — | Exact caller identity. |
| `session` | string | — | Exact `sessionId`. |
| `event` | string | — | `PreToolUse`, `PostToolUse`, or `model` (priced model turns). |
| `resolve` | `approvals` | — | For held rows, look up the approval and report its final state as the outcome (`approved`, `expired`, or `deny`). Bounded to 50 lookups per request. |

### Outcomes

A stored `decision` is what the gate said at the moment of the call. What *happened* to a held or paused call lives elsewhere: the approval record, the pause, the reason text. `outcome` folds those into one word so you can filter without knowing the internals:

| `outcome` | Means |
|---|---|
| `allow` / `deny` | The policy verdict, and nothing else happened. |
| `held` | The call created (or hit) an approval request and was not run. `approvalId` is on the row. |
| `approved` | An approved grant was consumed and the call ran (`pre-approved`), or, with `resolve=approvals`, a held call whose approval was later granted. |
| `expired` | With `resolve=approvals`: a held call whose approval timed out. |
| `paused` | Denied by the kill switch: the workspace or the agent was paused. |
| `ungoverned` | The call ran without a policy check (gateway unreachable, interactive tier fails open). |
| `flagged` | Allowed and marked for review. |
| `pass` / `redact` / `block` | PostToolUse outcomes on the tool's output. |
| `model` | A priced model turn (`llm.*` rows), from the proxy or from the harness transcript. |

Entries come back **newest first**, ordered by timestamp.

## Response

```json
{
  "entries": [
    {
      "id": "evt_9Qd2…",
      "tool": "github.create_issue",
      "decision": "deny",
      "decisionReason": "background tier denied by workspace policy",
      "agentName": "Campsite Scout",
      "agentTier": "background",
      "sub": "apikey:k_7f2a",
      "userEmail": "ops@acme.io",
      "sessionId": "run_Rk3p_Lm8_04",
      "requestId": "req_b81c…",
      "ts": "2026-06-25T17:22:09.044Z",
      "client": { "name": "Campsite Scout" }
    }
  ],
  "count": 1,
  "since": "2026-06-25T17:07:09.000Z",
  "limit": 200
}
```

### Entry fields

| Field | Description |
|-------|-------------|
| `id` | Unique entry ID. |
| `tool` | The tool that was called. |
| `decision` | What the gate said: `allow` or `deny` on a PreToolUse row; `pass`, `redact` or `block` on a PostToolUse row. |
| `decisionReason` | Human-readable reason for the decision. |
| `outcome` | The derived outcome (table above). |
| `approvalId` | On held and pre-approved rows: the approval this call created or consumed. |
| `usageSource` | On priced model rows: `transcript` when the hook reported the turn from the harness transcript rather than the proxy. |
| `agentName` | The agent that made the call. |
| `agentTier` | `interactive` · `subagent` · `background` · `api` — how it was running. |
| `sub` | The identity that made the call (`apikey:…`, a user UID, or a delegated subject). |
| `userEmail` | The human behind the call, when known. |
| `sessionId` | Groups all calls in one run. Join on this to reconstruct a session. |
| `requestId` | The individual request. |
| `ts` | ISO-8601 timestamp. |
| `client` | The connecting client app (`{ "name": … }`). |

### Delegation chain

For calls made by a delegated child key ([key delegation](/agents/quickstart)), the entry also carries the chain — so a single row reconstructs the full cryptographic ancestry:

| Field | Description |
|-------|-------------|
| `originSub` | The root identity the chain descends from. |
| `depth` | How many delegation hops deep this call is. |
| `chain` | The profile chain. |
| `runChain` | The run chain. |
| `parentProfileId` | The immediate parent profile. |

These are absent on non-delegated calls.

## Paginating

There's no cursor — page by **time**. Read a window, then set the next request's `since` just past the oldest `ts` you received (or pull forward from the newest on a tail). For a continuous feed, poll with `since` = the last `ts` you saw.

```python
import os, requests

ACP, slug = "https://api.agenticcontrolplane.com", "acme"
headers = {"Authorization": f"Bearer {os.environ['ACP_KEY']}"}
since = "2026-06-25T00:00:00Z"

while True:
    r = requests.get(f"{ACP}/{slug}/admin/audit",
                     params={"since": since, "limit": 1000}, headers=headers)
    entries = r.json()["entries"]
    if not entries:
        break
    handle(entries)                      # newest-first
    since = entries[0]["ts"]             # advance past the newest seen
```

## What you can't do here (yet)

This endpoint returns raw governed events. It does **not** yet expose the aggregated views the console computes on top of them — per-run cost X-ray (loop vs leaf), per-agent run-to-run variability, or runtime decomposition. Those are on the roadmap as a `/api/v1/runs` resource; until then, the console is the place to see them, and this endpoint is the raw material to compute your own. See the [API overview](/docs/api/) for where this is headed.

## Next

<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;">
  <a href="/docs/api/policies" class="acp-btn acp-btn-primary" style="padding:12px 24px;">Tune the policy &rarr;</a>
  <a href="/docs/api/" class="acp-btn acp-btn-ghost" style="padding:12px 24px;">API overview &rarr;</a>
</div>
