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
# 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
{
"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
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.ndjsonExample - JSON response shape
{
"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
{
"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
{
"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
{
"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