Audit API

Four endpoints against your operator's append-only hash-chained log. All require operator-scoped bearer auth. For the conceptual model, see/docs/concepts/audit.

GET /v1/audit

Tail of recent audit entries for the authenticated operator, ordered newest-first. Use this for live monitoring and incremental polling. For full date-range exports, use /v1/audit/export instead.

Query parameters

  • limit - integer 1–100 (default: 20)
  • since - epoch milliseconds; only return entries newer than this timestamp (optional)
  • agent_id - narrow to a single agent (optional)
  • passport_jti - narrow to a single passport (optional)

Example - tail one agent, polling every 2s

bash
# First request — get the watermark
curl -s "https://api.getstack.run/v1/audit?limit=20&agent_id=agt_support" \
  -H "Authorization: Bearer $STACK_API_KEY"

# Subsequent requests — only entries newer than `max_timestamp`
curl -s "https://api.getstack.run/v1/audit?since=$WATERMARK&agent_id=agt_support" \
  -H "Authorization: Bearer $STACK_API_KEY"

Response shape

json
{
  "entries": [
    {
      "entry_id": "aud_z9…",
      "timestamp": 1747913532614,
      "trace_id": "req_abc",
      "operator_id": "op_acme",
      "agent_id": "agt_support",
      "passport_jti": "pp_8f3a",
      "layer": "vault",
      "action": "passport.revoke_cascade",
      "outcome": "success",
      "duration_ms": 0,
      "error_code": null,
      "error_message": null,
      "prev_entry_hash": "ab3f…7c2e",
      "entry_hash": "c104…d8a1"
    }
  ],
  "max_timestamp": 1747913532614
}

Cascade revokes write one passport.revoke entry for the parent + one passport.revoke_cascade entry per child revoked. Filter by passport_jti to see the chain for a single passport.

GET /v1/audit/export

Paginated export of audit rows for the authenticated operator. Supports JSON (default), NDJSON (line-delimited JSON - stream-friendly), and CSV (spreadsheet-friendly). Each response is capped at 50,000 rows.

Query parameters

  • from - ISO-8601 datetime, inclusive lower bound on timestamp (optional)
  • to - ISO-8601 datetime, exclusive upper bound on timestamp (optional)
  • format - json | ndjson | csv (default: json)
  • limit - integer 1–50000 (default: 10000)
  • offset - integer ≥ 0 for pagination (default: 0)

Example - last 24 hours as NDJSON

bash
curl -s "https://api.getstack.run/v1/audit/export?from=$(date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%SZ)&format=ndjson" \
  -H "Authorization: Bearer $STACK_API_KEY" \
  > audit.ndjson

Example - JSON response shape

json
{
  "operator_id": "op_acme",
  "count": 2,
  "from": "2026-04-22T00:00:00Z",
  "to": null,
  "rows": [
    {
      "entry_id": "aud_A…",
      "timestamp": 1747913500000,
      "trace_id": "req_abc",
      "operator_id": "op_acme",
      "agent_id": "agt_support",
      "passport_jti": "pp_8f3a",
      "layer": "vault",
      "action": "passport.issue",
      "outcome": "success",
      "duration_ms": 42,
      "prev_entry_hash": "0000…0000",
      "entry_hash": "ab3f…7c2e"
    },
    {
      "entry_id": "aud_B…",
      "timestamp": 1747913532614,
      "layer": "vault",
      "action": "passport.revoke",
      "outcome": "success",
      "prev_entry_hash": "ab3f…7c2e",
      "entry_hash": "ab3f…81d3"
    }
  ]
}

GET /v1/audit/chain-head

Returns the most recent entry_hash for your operator along with the total number of rows. Anchor this value externally to prove later that your log was not retroactively rewritten.

Response

json
{
  "operator_id": "op_acme",
  "latest_entry_hash": "ab3f…81d3",
  "latest_timestamp": 1747913532614,
  "total_entries": 184723,
  "observed_at": "2026-04-23T14:32:12.603Z"
}

For brand-new operators with zero rows, latest_entry_hashand latest_timestamp are nulland total_entries is 0.

GET /v1/audit/verify-chain

Re-walks the chain for your operator and reports whether it is intact. Content tampering (mutated row fields) and chain tampering (reordered or missing prev_entry_hash links) both surface as a first_break pointer.

Query parameters

  • from - ISO-8601 datetime, inclusive lower bound (optional, defaults to the earliest retained row)
  • to - ISO-8601 datetime, exclusive upper bound (optional, defaults to now)
  • limit - integer 1–100000 (default: 10000)

Response - healthy chain

json
{
  "operator_id": "op_acme",
  "verified_at": "2026-04-23T14:35:00.000Z",
  "valid": true,
  "total_checked": 184723,
  "head_entry_hash": "ab3f…81d3"
}

Response - tampered chain

json
{
  "operator_id": "op_acme",
  "verified_at": "2026-04-23T14:35:00.000Z",
  "valid": false,
  "total_checked": 912,
  "head_entry_hash": "ab3f…81d3",
  "first_break": {
    "entry_id": "aud_X…",
    "timestamp": 1747912000000,
    "reason": "hash_mismatch",
    "expected": "d48a…1f02",
    "actual": "ee91…22c5"
  }
}

Two failure reasons are reported:

  • hash_mismatch - this row's entry_hash does not match a freshly-computed hash of its fields (content was changed)
  • prev_hash_mismatch - this row's prev_entry_hash does not equal the prior row's entry_hash (a row was inserted, deleted, or reordered)

Authentication & scope

  • All three endpoints require a valid operator API key
  • Responses include only rows where operator_id matches the authenticated operator
  • Team member keys inherit the parent operator's visibility
stack | docs