Audit log
Every decision Writ makes — ALLOW, DENY, STEP_UP, REVOKE — lands in your tenant’s audit log as a receipt. The log is the product’s answer to the auditor: instead of dumping CloudTrail and hoping, the compliance owner opens the receipt and sees who, what, which purpose, which policy version, when.
Watch it live
Three ways, same stream:
Or poll GET /v1/receipts?since=<receiptId> — it returns only newer
receipts plus a cursor.
Filter by decision
GET /v1/receipts/stream?decision=DENY streams only one decision —
ALLOW, DENY, or STEP_UP (case-insensitive; anything else is a 400).
The filter applies to the initial replay and to every frame emitted while
the connection is held.
The DENY-only stream shows every write Writ blocked before it happened, in real time. The dashboard’s Denied panel is built on it — verb, target, and reason for each stopped write, with a running count.
Streaming to your SIEM
Security teams don’t want another dashboard — they want the event stream.
GET /v1/receipts/stream is that feed: every gated write your tenant’s
agents attempt, as server-sent events, in near real time. This is the
answer to “can we pipe this into our SIEM?”
Connecting. Authenticate with Authorization: Bearer <key>, or pass
?apiKey= — browsers’ EventSource can’t set headers, which is why the
query param exists. The server holds the connection up to timeout
seconds (default 10, max 12), emitting event: receipt frames plus
: ping heartbeats, then closes it. Reconnect and resume with since
(or Last-Event-ID, which EventSource sends automatically). Omit
since on first connect to replay the most recent replay receipts
(default 20) and then go live.
What each event carries. Every frame’s data is a complete receipt:
receiptId, tenantId, decision (ALLOW, DENY, STEP_UP, REVOKE),
sponsorId, agentId, verb, target, purpose, reason,
policyVersion, createdAt, plus prevHash/chainHash for the
tamper-evident chain. No joins needed downstream — enrich or index it as
is. Use receiptId as the dedup key; the resume cursor guarantees at-least-once delivery across reconnects, never gaps.
Filtering. Append ?decision=DENY to stream only stopped writes —
the feed your SOC actually wants to watch. ALLOW and STEP_UP work
too; anything else is a 400. The filter applies to the replay and the
live hold alike.
Tenant isolation. One tenant’s stream never carries another tenant’s receipts. A receipt ID from another tenant is indistinguishable from a missing one everywhere in the API.
Polling fallback. Where SSE is unavailable, poll
GET /v1/receipts?since=<cursor> — it returns only newer receipts plus a
cursor for the next poll.
Tamper evidence
Every receipt is hash-chained: each one stores prevHash (the previous
receipt’s chainHash, or GENESIS for the first) and
chainHash = HMAC-SHA256(canonical receipt fields + prevHash), keyed by a
server-side secret held only in the deploy environment. The tenant’s chain
head advances with a conditional write, so concurrent checks serialize into one linear chain
instead of forking. The canonical field set is version-stable: attribution
fields added after launch (policyId, principalId, …) are omitted from the
hash when unset, so older receipts keep verifying.
Anyone with raw table access who alters a stored receipt breaks the chain — and the break is detectable:
GET /v1/receipts/verify returns
{"ok": true, "checked": N, "tip": "<hash>", "tipReceipt": "<id>", "chainedFrom": "<id>", "legacyCount": M, "complete": true} when the chain
is intact, or {"ok": false, "brokenAt": "<receiptId>", "reason": "..."}
at the first receipt that fails.
Receipts written before chaining was enabled have no chain fields;
verification stops at them and reports them as legacyCount.
How the chain stays unforgeable
The chain HMAC is keyed by a server-side secret that never leaves the gate’s deploy environment and is never exposed through the API. Whoever holds the receipt table but not that secret cannot forge a valid link: any altered receipt fails verification at the first changed hash.
This is the control that supports NIST SP 800-53 AU-9 (Protection of Audit Information): audit records are protected against unauthorized modification and deletion, and tampering is detectable on demand. It also feeds AU-3 (content of audit records) — every receipt records who, what, when, and why — and AU-12 (audit generation). Writ is not itself certified; your assessor determines compliance for your deployment, but the chain gives them the tamper-evidence mechanism AU-9 asks for.
Agent Action Report
The audit log is the raw material. The Agent Action Report is the executive artifact — one page a security lead can forward upward.
Example output (illustrative — your numbers come from your own receipts):
What it aggregates:
- Actions — total attempted, allowed, denied, escalated (
--agentfilters to one agent;--since YYYY-MM-DDbounds the window;--periodsets the label). - Money — for money verbs (
payments.*,billing.*,refund*,payout*,charge*,transfer*), the report sums dollar amounts from receiptamountfields or from the target string (amount=5000,$50.00,5000 cents). “Attempted” vs “denied by policy” is the number your finance team cares about. - Revocations — permission revocations in the window.
- Unauthorized writes blocked — every DENY is a write Writ stopped before it happened.
- By verb — per-verb attempted/allowed/denied breakdown.
Billing note
The audit log is also the meter: every receipt is a billable event under commit + volume pricing, and because you hold the same tamper-evident log, you can verify usage yourself. See Pricing.