Exporting events
An export writes one org's events, cold storage included, to one CSV or NDJSON file in seq order. The download URL lasts 24 hours.
List and get only read the hot log, which holds events for the plan's hot window. An export also reads cold storage, so an old range comes back whole.
Exports run in the background. You create a job, poll it, then fetch the file from a presigned URL. The filters are the ones list accepts.
Pick NDJSON when a program will read the file or verify it. Pick CSV for a spreadsheet; its rows carry the hash and signature but not every signed field.
| Scope | audit:read |
|---|---|
| Formats | csv or ndjson |
| Filters | actions, actor_id, target_id, occurred_after, occurred_before |
| Order | seq ascending: cold storage segments first, then the hot log |
| Worker | picks up pending jobs about every 10 seconds |
| Download URL | presigned for 24 hours from each GET |
| Content-Type | text/csv or application/x-ndjson |
Create
POST /v1/audit/exports with organization_id, format and optional filters. The reply is 202 with the aexp_ id and status pending.
Poll
GET /v1/audit/exports/{id} until status is ready or failed. It goes pending, running, then ready or failed; error holds the failure message.
Download
GET download_url within 24 hours. Each poll presigns a fresh URL, so call again if the link expired.
NDJSON
One event per line, the same JSON that get returns, in seq order. The file ends in .ndjson and is served as application/x-ndjson.
{"id":"aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK","org_id":"aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM","seq":42,"schema_id":"invoance.audit/1","occurred_at":"2026-09-22T08:14:07.000Z","ingested_at":"2026-09-22T08:14:07.312Z","action":"user.signed_in","actor":{"type":"user","id":"u_4821","name":"Ada Lovelace"},"targets":[{"type":"workspace","id":"ws_17"}],"context":{"location":"203.0.113.10","user_agent":"Mozilla/5.0"},"metadata":{"method":"sso","mfa":true},"payload_hash":"df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3","signature":"c6a2bf3bc3895915ead5f0b99eaf84534d4e8b9aa68bef8b0508cfd473f06e694d76d2bc330b83cfa613e49e7d2490d0413285017acfb78746bcc1524d05160d","signing_public_key":"bee215a9d2a0170176a88733056d8a0ae5372b0da8238796bef4a0b75d4c0974"}
CSV
A header row and one row per event. targets holds type:id pairs joined by a bar. A cell that starts with =, +, -, @, tab or carriage return gets a leading quote, so a spreadsheet shows it as text.
id,seq,occurred_at,ingested_at,action,actor_type,actor_id,actor_name,targets,context_location,payload_hash,signature
aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK,42,2026-09-22T08:14:07.000Z,2026-09-22T08:14:07.312Z,user.signed_in,user,u_4821,Ada Lovelace,workspace:ws_17,203.0.113.10,df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3,c6a2bf3bc3895915ead5f0b99eaf84534d4e8b9aa68bef8b0508cfd473f06e694d76d2bc330b83cfa613e49e7d2490d0413285017acfb78746bcc1524d05160d
Not in the CSV: org_id, context.user_agent, metadata, target names and metadata, signing_public_key. A CSV row cannot be verified offline.
Each NDJSON line is a complete event. The SDK verifiers rebuild its canonical bytes and check payload_hash and the Ed25519 signature, with no network call.
By default they use the signing_public_key on the line. Pass your registered key from GET /keys/{domain} to pin it, which is what the server does.
Without an SDK, POST each line to /v1/proof/audit/{event_id}/verify; it needs no API key.
Node.js | import { verifyAuditEvent } from "invoance" |
|---|---|
| Python | from invoance import verify_audit_event |
| Go | invoance.VerifyAuditEvent |
| Java | com.invoance.AuditVerify.verifyAuditEvent |
| Ruby | Invoance::AuditVerify.verify_audit_event |
| Rust | invoance::verify_audit_event |
| .NET | Invoance.Internal.AuditVerify.VerifyAuditEvent |
| PHP | Invoance\AuditVerify::verifyAuditEvent |
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import { verifyAuditEvent } from "invoance";
// Your registered key from GET /keys/{domain}, as hex. Leave it unset to
// check against the signing_public_key carried on each line.
const publicKey = process.env.INVOANCE_PUBLIC_KEY;
let valid = 0;
let invalid = 0;
const lines = createInterface({ input: createReadStream("export.ndjson") });
for await (const line of lines) {
if (!line) continue;
const event = JSON.parse(line);
const result = verifyAuditEvent(event, { publicKey });
if (result.valid) {
valid += 1;
} else {
invalid += 1;
console.log(event.id, result.reason);
}
}
console.log("valid", valid, "invalid", invalid);