Home
Home/Developers/Audit logs
Ed25519 signaturesSHA-256 hashes8 SDKs
Status
Sign inStart free
Home
Start here
Overview
Authentication
Errors
FAQ
API
Events
Canonical JSON and hashes
Documents
Anchoring a file
AI attestations
Attestation schema
Verifying attestations
Traces
Sealing a trace
Audit logs
Organizations
Streams
Portal
Public proof
Event schema
Exporting events
Integrations
Clerk
Auth0
Embeddable viewer
All endpoints
Reference
SDKs
Node.js
Python
Go
Java
Ruby
Rust
.NET
PHP
REST
Verification
API · Audit Logs

Audit Logs

Append signed activity records for each of your end-customer orgs, then read, stream, export and verify them.

sha-256 · 3a352297…2592
sha-256 · bae251a9…8ffa

An audit event says who did what to which targets, and when. You register an audit org for each end customer (your own organization_id, mapped to an aorg_ id), then POST events for it with an action such as user.signed_in, an actor, up to 20 targets, optional context and flat metadata, and an Idempotency-Key. The backend checks the envelope, counts the event against the monthly audit quota, mints an aevt_ id, and replies 201 as soon as the event is on a durable queue.

A worker then assigns the org's next seq (a per-org counter with no gaps), builds the signed object (org_id, event_id, seq, ingested_at, action, occurred_at, actor, targets, context, metadata and the constant schema_id invoance.audit/1), serializes it canonically (nulls removed, keys sorted, compact), hashes it with SHA-256 into payload_hash and signs the bytes with the tenant's Ed25519 key. The signature and public key are stored with the row and returned by every read.

Verification recomputes the canonical bytes from the row (or from a copy you hold) and checks the hash and the signature against the key registered for the tenant, not the key on the row. Rows are never edited; after the plan's hot window they move to cold storage and after retention_days they are purged, with evacuated_through_seq recording the floor so the integrity scan can tell purged from missing.

Endpoints
EventsPOST/v1/audit/eventsIngest an audit eventGET/v1/audit/eventsList audit eventsGET/v1/audit/events/{id}Get an audit eventGET/v1/audit/events/{id}/verifyVerify an audit event
OrganizationsPOST/v1/audit/orgsCreate an audit orgGET/v1/audit/orgsList audit orgsPATCH/v1/audit/orgs/{id}Rename an audit orgDELETE/v1/audit/orgs/{id}Delete an audit orgPOST/v1/audit/orgs/{id}/archiveArchive an audit orgPOST/v1/audit/orgs/{id}/unarchiveUnarchive an audit orgGET/v1/audit/orgs/{id}/integrityCheck an org's sequence integrityPUT/v1/audit/orgs/{id}/retentionSet an org's retention
StreamsPOST/v1/audit/orgs/{id}/streamsCreate a webhook streamGET/v1/audit/orgs/{id}/streamsList an org's streamsDELETE/v1/audit/orgs/{id}/streams/{stream_id}Delete a streamPOST/v1/audit/orgs/{id}/streams/{stream_id}/testSend a test delivery
Portal sessionsPOST/v1/audit/portal_sessionsCreate a portal session
ExportsPOST/v1/audit/exportsCreate an exportGET/v1/audit/exports/{id}Get an export
Portal, with a portal tokenPOST/v1/audit/portal/exchangeExchange a portal link tokenGET/v1/audit/portal/eventsList events through the portalGET/v1/audit/portal/events/{id}Get an event through the portalGET/v1/audit/portal/events/{id}/verifyVerify an event through the portalGET/v1/audit/portal/orgGet the portal's org and issuerGET/v1/audit/portal/streamsList streams through the portalPOST/v1/audit/portal/streamsCreate a stream through the portalDELETE/v1/audit/portal/streams/{id}Delete a stream through the portalPOST/v1/audit/portal/streams/{id}/testTest a stream through the portal
Public proofGET/v1/proof/audit/{event_id}Get the public proof of an audit eventPOST/v1/proof/audit/{event_id}/verifyVerify a copy of an audit event
Limits
Ingest bodyup to 64 KB (65,536 bytes) of JSON; duplicate keys anywhere are rejected
Idempotency-Key headerrequired on ingest; 1 to 255 characters after trimming
action1 to 128 characters matching ^[a-z0-9_]+(\.[a-z0-9_]+){1,2}$ (two or three dotted lower-snake segments)
occurred_atRFC 3339, between now minus 5 years and now plus 24 hours
targetsup to 20 entries, each with a non-empty type and id
metadata (event, actor and each target)up to 50 keys; keys up to 40 characters; values string, boolean or 64-bit integer up to 500 characters serialized; floats, arrays and nested objects rejected
organization_id and name on org createeach up to 255 characters
Event list page sizelimit clamped to 1 to 100; default 50
Org list and stream listup to 100 rows, newest first, no pagination
Integrity gapsup to 1,000 gap ranges per response, gaps_truncated set when the cap is hit
Audit events per monthset by plan; seeded as developer 10,000, builder 1,000,000, growth 5,000,000, compliance 25,000,000, enterprise unlimited, plus purchased top-ups
Audit orgs per tenantset by plan; seeded as developer 2, builder 1,000, growth 3,000, compliance 10,000, enterprise unlimited
Streams per orgset by plan; seeded as developer 1, builder 2, growth 5, compliance 20, enterprise unlimited
Retention capthe plan's retention_days: developer 30, builder 365, growth 2,555, compliance 2,555, enterprise 36,500; a new org starts at 90 days or the cap, whichever is lower
Hot windowdays an event stays in Postgres before moving to cold storage: developer 30, builder 90, growth 90, compliance 180, enterprise 365
Webhook deliveryup to 100 events per POST in seq order, 15 second timeout, no redirects, retry backoff up to 300 seconds
Portal linkopen window 60 to 3,600 seconds (default 300); viewer session 60 to 86,400 seconds (default 7,200)
Portal rate limitper IP: 10 requests per second on exchange, 60 per second on reads
Export download URLpresigned for 24 hours from each GET
Requests per secondset by plan, default 10; the per-minute budget is 60 times that, shared by all keys of a tenant
Events
POST/v1/audit/events
API keyaudit:writeIdempotency-Key

Ingest an audit event

Accepts one audit event for an existing org, queues it for sequencing and signing, and returns the new event_id with its ingested_at timestamp.

Headers
Content-Typestring · required

Must be application/json.

Idempotency-Keystring · required · 1 to 255 characters after trimming

Client-chosen key that makes retries safe; the same key with the same body replays the stored 201 for 24 hours, and the key is scoped to the org, the API key and this endpoint.

Request body
organization_idstring · required · must resolve to an existing org that is not archived

Your id for the end customer as registered with create org, or the aorg_ id; org is accepted as a legacy alias.

actionstring · required · 1 to 128 characters matching ^[a-z0-9_]+(\.[a-z0-9_]+){1,2}$

What happened, as two or three dotted lower-snake segments, for example user.signed_in or billing.plan.changed.

occurred_attimestamp (ISO 8601) · required · between now minus 5 years and now plus 24 hours

When the event happened in your system, RFC 3339 with any offset; normalized to UTC with three fractional digits (truncated) before signing.

actorobject · required

Who performed the action.

actor.typestring · required

Kind of principal, for example user, api_key or system; must not be empty.

actor.idstring · required

Your identifier for the principal; must not be empty.

actor.namestring

Display name for the principal.

actor.metadataobject · up to 50 keys, key up to 40 characters, scalar values up to 500 characters

Flat key-value details about the actor, under the same rules as the top-level metadata.

targetsobject[] · required · up to 20 entries

What the action was applied to; send an empty array when there is no target.

targets[].typestring · required

Kind of target; must not be empty.

targets[].idstring · required

Your identifier for the target; must not be empty.

targets[].namestring

Display name for the target.

targets[].metadataobject · up to 50 keys, key up to 40 characters, scalar values up to 500 characters

Flat key-value details about the target, under the same rules as the top-level metadata.

contextobject

Where the action came from; only the two keys below are accepted.

context.locationstring

The actor's IP address or location string.

context.user_agentstring

The actor's user agent string.

metadataobject · up to 50 keys; keys up to 40 characters; values string, boolean or 64-bit integer up to 500 characters serialized; floats, arrays and nested objects are rejected

Flat key-value details about the event; null values are dropped from the signed bytes.

versioninteger

Reserved envelope version; must equal 1 when present and is not part of the signed bytes. One of 1.

  • The 201 means the event is on a durable queue; a worker assigns seq, signs and writes the row moments later, so a get right after ingest can return 404 not_found until the row lands.
  • Checks run in this order: scope, Idempotency-Key, body size, JSON parse, envelope, field validation, occurred_at window, org lookup, monthly quota, idempotency lookup, publish; a request that fails before the org lookup consumes no quota.
  • The quota is counted before the idempotency lookup, so a replay of an already stored request still counts one event for the period.
  • The idempotency request hash covers the resolved org id, action, the normalized occurred_at and the raw body bytes, so a retry with the same key but different whitespace or key order gets 409 idempotency_key_reuse_mismatch.
  • A retry with the same Idempotency-Key while the first request is still in flight gets 202 with body {"status": "processing"}; once the first request completes, the stored 201 body is replayed for 24 hours.
  • The signed object is org_id, event_id, seq, ingested_at, action, occurred_at, actor, targets, context and metadata plus schema_id invoance.audit/1; organization_id as you sent it and version are not signed, and null metadata values are dropped before signing.
  • When you omit idempotencyKey, every SDK derives one as idem_ plus the SHA-256 of the sorted, compact body, so two calls with byte-identical content (including occurred_at) map to one stored event; pass your own key when identical events must be stored twice.
  • The Go SDK defaults occurred_at to whole seconds and the PHP SDK to a Z timestamp without fractions; the backend accepts both and normalizes to three fractional digits.
  • The SDKs default targets to an empty array when omitted; the backend itself requires the key to be present.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const result = await client.audit.events.ingest({
  organizationId: "org_8472",
  action: "user.signed_in",
  occurredAt: "2026-09-22T08:14:07Z",
  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 },
  idempotencyKey: "signin-u_4821-2026-09-22T08:14:07Z",
});
console.log(result.event_id, result.ingested_at);
Response · 201
{
  "event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
  "ingested_at": "2026-09-22T08:14:07.312Z"
}
Response fields
event_idstring

Id assigned to the event, aevt_ followed by a ULID; use it with get, verify and the public proof page.

ingested_attimestamp (ISO 8601)

Server time at which the event was accepted, UTC with three fractional digits; this is the timestamp that gets signed.

Errors
missing_idempotency_key400

The Idempotency-Key header is absent or empty after trimming.

invalid_idempotency_key400

The Idempotency-Key is longer than 255 characters.

payload_too_large400

The request body is larger than 65,536 bytes.

invalid_json400

The body is not valid UTF-8 or not valid JSON.

duplicate_json_key400

The same key appears twice in one JSON object anywhere in the body.

invalid_envelope400

A required field is missing, a field has the wrong type, or an unknown key appears at the top level, in actor, in a target or in context.

invalid_org400

organization_id is empty after trimming.

invalid_action400

action is empty, longer than 128 characters, or does not match the two-to-three-segment grammar.

invalid_actor400

actor.type or actor.id is empty after trimming.

too_many_targets400

targets has more than 20 entries.

invalid_target400

A target has an empty type or id.

invalid_version400

version is present and not 1.

invalid_metadata400

metadata (on the event, the actor or a target) is not an object, has more than 50 keys, a key over 40 characters, a value over 500 characters, a float, an array or a nested object.

invalid_occurred_at400

occurred_at is not a valid RFC 3339 timestamp.

occurred_at_out_of_window400

occurred_at is more than 5 years in the past or more than 24 hours in the future.

invalid_api_context401

The request was authenticated without an API key id; not reachable with a normal key.

payment_required402

The tenant is on a paid plan whose subscription is past due.

insufficient_scope403

The key does not have audit:write; audit:read alone is not enough.

feature_not_available403

The plan's audit_events_per_month limit is 0.

org_not_found404

No org with that organization_id or aorg_ id belongs to the tenant; orgs are never created by ingest.

org_archived409

The org is archived; unarchive it first.

idempotency_key_reuse_mismatch409

The Idempotency-Key was already used for this org by this API key with a different action, occurred_at or body bytes.

quota_exceeded429

The tenant has used its monthly audit events quota for the current billing period.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

The org lookup failed.

quota_check_failed500

The quota counter could not be read; the request is rejected rather than allowed through.

encode_failed500

The queue envelope could not be serialized; the idempotency reservation is released.

ingest_failed500

The event could not be published to the queue; the idempotency reservation is released so a retry can succeed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

GET/v1/audit/events
API keyaudit:read

List audit events

Returns one org's events newest first with optional filters, plus a cursor for the next page.

Query parameters
organization_idstring · required

Your organization_id or the aorg_ id of the org to read; org_id is accepted as a legacy alias.

actionsstring

Comma-separated list of action strings; only events whose action is in the list are returned.

actor_idstring

Only events whose actor.id equals this value.

target_idstring

Only events with a target whose id equals this value.

range_starttimestamp (ISO 8601)

Inclusive lower bound on occurred_at, RFC 3339; occurred_after is accepted as an alias.

range_endtimestamp (ISO 8601)

Inclusive upper bound on occurred_at, RFC 3339; occurred_before is accepted as an alias.

limitinteger · clamped to 1 to 100 · default 50

Page size.

cursorstring

Opaque next_cursor from the previous page.

  • Pagination is keyset on (occurred_at, id); the cursor is the hex encoding of "<occurred_at>|<id>" of the last row, and new rows always land on the first page, so paging never skips or repeats a row.
  • The page holds at most limit rows; next_cursor is null when no more rows match.
  • organization_id is required by the backend; the SDKs type it as optional, and a call without it is rejected by the framework with a plain-text 400 before the handler runs.
  • actor_id compares against the stored actor.id; target_id matches any entry of targets by id.
  • Archived orgs stay readable here; only writes are blocked while an org is archived.
  • Events past the plan's hot window are no longer in this list; use an export to read cold storage.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const page = await client.audit.events.list({
  organizationId: "org_8472",
  actions: "user.signed_in,user.signed_out",
  rangeStart: "2026-09-01T00:00:00Z",
  limit: 50,
});
for (const event of page.events) {
  console.log(event.seq, event.action, event.actor?.id);
}
console.log(page.next_cursor);
Response · 200
{
  "events": [
    {
      "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"
    }
  ],
  "next_cursor": "323032362d30392d32325430383a31343a30372e3030305a7c616576745f30314a3059315a32413342344335443645374638473948304a4b"
}
Response fields
eventsobject[]

The page of events, ordered by occurred_at descending then id descending.

events[].idstring

Event id, aevt_ followed by a ULID; minted at ingest before the event is queued.

events[].org_idstring

The aorg_ id of the audit org the event belongs to (not your organization_id).

events[].seqinteger

Position in the org's log, assigned by the signer in commit order; per org it starts at 1 and has no gaps.

events[].schema_idstring

Always invoance.audit/1; it is inside the signed bytes as a domain tag. One of invoance.audit/1.

events[].occurred_attimestamp (ISO 8601)

When the event happened in your system, normalized to UTC with exactly three fractional digits and a Z suffix.

events[].ingested_attimestamp (ISO 8601)

Server time when the event was accepted, in the same canonical form; this value is signed.

events[].actionstring

The action string as sent, for example user.signed_in.

events[].actorobject

The actor object as sent: type and id, plus name and metadata when given.

events[].targetsobject[]

The targets array as sent; empty when the event had none.

events[].contextobject

The context object as sent (location, user_agent), or null when omitted.

events[].metadataobject

The flat metadata object as sent, or null when omitted.

events[].payload_hashhex

SHA-256 of the canonical signed bytes, 64 hex characters.

events[].signaturehex

Ed25519 signature over the canonical bytes, 128 hex characters.

events[].signing_public_keyhex

The tenant's Ed25519 public key as recorded with the row, 64 hex characters; shown for display, verification uses the key registered in tenant_keys.

next_cursorstring

Opaque cursor to pass as ?cursor= for the next page, or null on the last page.

Errors
invalid_organization_id400

organization_id is empty after trimming.

unknown_org400

No org with that organization_id or aorg_ id belongs to the tenant.

invalid_timestamp400

range_start or range_end is not RFC 3339.

invalid_cursor400

cursor is not a cursor this endpoint issued.

insufficient_scope403

The key has neither audit:read nor audit:write; a ledger key with only read or write is rejected on audit routes.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

A database query failed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

GET/v1/audit/events/{id}
API keyaudit:read

Get an audit event

Returns one event with its hash, signature and signing public key.

Path parameters
idstring · required

The aevt_ id returned by ingest.

  • An id that exists under another tenant returns the same 404 as an unknown id.
  • Every SDK ships an offline verifier (verifyAuditEvent / verify_audit_event) that rebuilds the canonical bytes from this JSON and checks the signature; pass the key from GET /keys/{domain} as publicKey to pin it, otherwise the row's own signing_public_key is used.
  • Events past the plan's hot window return 404 here even though they are still in cold storage; exports read cold storage.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const event = await client.audit.events.get("aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK");
console.log(event.seq, event.action, event.payload_hash);
Response · 200
{
  "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"
}
Response fields
idstring

Event id, aevt_ followed by a ULID; minted at ingest before the event is queued.

org_idstring

The aorg_ id of the audit org the event belongs to (not your organization_id).

seqinteger

Position in the org's log, assigned by the signer in commit order; per org it starts at 1 and has no gaps.

schema_idstring

Always invoance.audit/1; it is inside the signed bytes as a domain tag. One of invoance.audit/1.

occurred_attimestamp (ISO 8601)

When the event happened in your system, normalized to UTC with exactly three fractional digits and a Z suffix.

ingested_attimestamp (ISO 8601)

Server time when the event was accepted, in the same canonical form; this value is signed.

actionstring

The action string as sent, for example user.signed_in.

actorobject

The actor object as sent: type and id, plus name and metadata when given.

targetsobject[]

The targets array as sent; empty when the event had none.

contextobject

The context object as sent (location, user_agent), or null when omitted.

metadataobject

The flat metadata object as sent, or null when omitted.

payload_hashhex

SHA-256 of the canonical signed bytes, 64 hex characters.

signaturehex

Ed25519 signature over the canonical bytes, 128 hex characters.

signing_public_keyhex

The tenant's Ed25519 public key as recorded with the row, 64 hex characters; shown for display, verification uses the key registered in tenant_keys.

Errors
not_found404

No event with that id belongs to the tenant, or the worker has not written it yet.

insufficient_scope403

The key has neither audit:read nor audit:write; a ledger key with only read or write is rejected on audit routes.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

A database query failed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

GET/v1/audit/events/{id}/verify
API keyaudit:read

Verify an audit event

Recomputes the canonical bytes from the stored row and checks the hash and the Ed25519 signature against the tenant's registered key.

Path parameters
idstring · required

The aevt_ id of the event to verify.

  • This is a GET with no body, unlike POST /v1/events/{event_id}/verify; there is nothing to submit because the row already holds everything that was signed.
  • A row whose signature was replaced with one made by a different key fails with signature_invalid even when the row's own signing_public_key matches, because the check pins the key in tenant_keys.
  • The order of checks is hash, then domain tag, then signature; reason names the first check that failed.
  • Calls to this endpoint are not counted against the api_verifications quota.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const result = await client.audit.events.verify("aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK");
console.log(result.valid, result.reason, result.payload_hash);
Response · 200
{
  "event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
  "valid": true,
  "reason": null,
  "schema": "invoance.audit/1",
  "payload_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
  "signed_data": {
    "action": "user.signed_in",
    "actor": {
      "id": "u_4821",
      "name": "Ada Lovelace",
      "type": "user"
    },
    "context": {
      "location": "203.0.113.10",
      "user_agent": "Mozilla/5.0"
    },
    "event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
    "ingested_at": "2026-09-22T08:14:07.312Z",
    "metadata": {
      "method": "sso",
      "mfa": true
    },
    "occurred_at": "2026-09-22T08:14:07.000Z",
    "org_id": "aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM",
    "schema_id": "invoance.audit/1",
    "seq": 42,
    "targets": [
      {
        "id": "ws_17",
        "type": "workspace"
      }
    ]
  },
  "key_source": "tenant_keys"
}
Response fields
event_idstring

The id from the path.

validboolean

True when the recomputed hash matches the stored payload_hash, the bytes carry the audit schema_id, and the signature verifies under the tenant's registered key.

reasonstring

Why the check failed, or null when valid. One of canonicalization_failed, payload_hash_mismatch, wrong_domain, signature_invalid.

schemastring

Always invoance.audit/1. One of invoance.audit/1.

payload_hashhex

SHA-256 of the recomputed canonical bytes; empty when canonicalization failed.

signed_dataobject

The canonical signed object parsed back to JSON (keys sorted, nulls removed, schema_id included); null when canonicalization failed.

key_sourcestring

Always tenant_keys: the key used is the tenant's registered key, never the one stored on the row. One of tenant_keys.

Errors
not_found404

No event with that id belongs to the tenant.

insufficient_scope403

The key has neither audit:read nor audit:write; a ledger key with only read or write is rejected on audit routes.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

A database query failed, or the tenant has no registered key in tenant_keys.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

Exports
POST/v1/audit/exports
API keyaudit:read

Create an export

Queues a job that writes the org's events matching the filters, cold storage and hot log in seq order, to one CSV or NDJSON file, and returns the job id to poll.

Headers
Content-Typestring · required

Must be application/json.

Request body
organization_idstring · required

Your organization_id or the aorg_ id of the org to export; org_id is accepted as a legacy alias.

formatstring · required

Output format. One of csv, ndjson.

filtersobject

Optional filters; only the keys below are accepted.

filters.actionsstring

Comma-separated action strings; only matching events are exported.

filters.actor_idstring

Only events whose actor.id equals this value.

filters.target_idstring

Only events with a target whose id equals this value.

filters.occurred_aftertimestamp (ISO 8601)

Inclusive lower bound on occurred_at, RFC 3339; range_start is accepted as an alias.

filters.occurred_beforetimestamp (ISO 8601)

Inclusive upper bound on occurred_at, RFC 3339; range_end is accepted as an alias.

  • This endpoint needs only audit:read, unlike the other POST routes of the resource.
  • A worker polls for pending jobs about every 10 seconds, moves the job to running, writes the file and marks it ready or failed; poll GET /v1/audit/exports/{id} until status is ready or failed.
  • Unknown keys inside filters are rejected by the framework with a plain-text 4xx before the handler runs; a malformed timestamp in filters is accepted here and fails the job later with status failed.
  • NDJSON lines are the same event JSON that get returns, one per line; CSV columns are id, seq, occurred_at, ingested_at, action, actor_type, actor_id, actor_name, targets, context_location, payload_hash, signature, with cells that start with =, +, -, @, tab or carriage return prefixed to block spreadsheet formula injection.
  • Exports include events that left the hot log for cold storage, which list and get no longer return.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const job = await client.audit.exports.create({
  organizationId: "org_8472",
  format: "ndjson",
  filters: {
    actions: "user.signed_in,user.signed_out",
    occurred_after: "2026-09-01T00:00:00Z",
  },
});
console.log(job.id, job.status);
Response · 202
{
  "id": "aexp_01J0Y4Q6S8V0X2Z4B6D8F0H2JK",
  "status": "pending",
  "format": "ndjson"
}
Response fields
idstring

Export id, aexp_ followed by a ULID; poll it with get export.

statusstring

Always pending on creation. One of pending.

formatstring

The format you requested. One of csv, ndjson.

Errors
invalid_format400

format is neither csv nor ndjson.

insufficient_scope403

The key has neither audit:read nor audit:write; a ledger key with only read or write is rejected on audit routes.

not_found404

No org with that organization_id or aorg_ id belongs to the tenant.

org_archived409

The org is archived; unarchive it first.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

A database query failed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

GET/v1/audit/exports/{id}
API keyaudit:read

Get an export

Returns the job's status and, once it is ready, a presigned download URL valid for 24 hours.

Path parameters
idstring · required

The aexp_ id returned by create export.

  • Each GET presigns a fresh URL, so a client that lost the link can call again.
  • The file is served with Content-Type text/csv or application/x-ndjson.
  • Exports are deleted with their org; nothing else removes them.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

let job = await client.audit.exports.get("aexp_01J0Y4Q6S8V0X2Z4B6D8F0H2JK");
while (job.status === "pending" || job.status === "running") {
  await new Promise((r) => setTimeout(r, 5000));
  job = await client.audit.exports.get("aexp_01J0Y4Q6S8V0X2Z4B6D8F0H2JK");
}
console.log(job.status, job.row_count, job.download_url ?? job.error);
Response · 200
{
  "id": "aexp_01J0Y4Q6S8V0X2Z4B6D8F0H2JK",
  "status": "ready",
  "format": "ndjson",
  "row_count": 1284,
  "error": null,
  "created_at": "2026-09-22T08:20:14.201457+00:00",
  "completed_at": "2026-09-22T08:20:31.884016+00:00",
  "download_url": "https://storage.example.com/tenants/3b9d6f10-52c4-4a7e-9e1b-8d0c2f4a6e71/audit/exports/aexp_01J0Y4Q6S8V0X2Z4B6D8F0H2JK.ndjson?X-Amz-Expires=86400&X-Amz-Signature=8f2b1c"
}
Response fields
idstring

The export id.

statusstring

pending until a worker claims the job, running while the file is written, then ready or failed. One of pending, running, ready, failed.

formatstring

The requested format. One of csv, ndjson.

row_countinteger

Events written to the file; null until the job is ready.

errorstring

Failure message (up to 500 characters) when status is failed, otherwise null.

created_attimestamp (ISO 8601)

When the job was queued.

completed_attimestamp (ISO 8601)

When the job finished, ready or failed; null before that.

download_urlstring

Presigned GET URL for the file, valid 24 hours from this response; null unless status is ready, or when presigning failed.

Errors
insufficient_scope403

The key has neither audit:read nor audit:write; a ledger key with only read or write is rejected on audit routes.

not_found404

No export with that id belongs to the tenant.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

A database query failed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

An Authorization header was sent without the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

More endpoints
OrganizationsPOST/v1/audit/orgsCreate an audit orgGET/v1/audit/orgsList audit orgsPATCH/v1/audit/orgs/{id}Rename an audit orgDELETE/v1/audit/orgs/{id}Delete an audit orgPOST/v1/audit/orgs/{id}/archiveArchive an audit orgPOST/v1/audit/orgs/{id}/unarchiveUnarchive an audit orgGET/v1/audit/orgs/{id}/integrityCheck an org's sequence integrityPUT/v1/audit/orgs/{id}/retentionSet an org's retention
StreamsPOST/v1/audit/orgs/{id}/streamsCreate a webhook streamGET/v1/audit/orgs/{id}/streamsList an org's streamsDELETE/v1/audit/orgs/{id}/streams/{stream_id}Delete a streamPOST/v1/audit/orgs/{id}/streams/{stream_id}/testSend a test delivery
Portal sessionsPOST/v1/audit/portal_sessionsCreate a portal session
Portal, with a portal tokenPOST/v1/audit/portal/exchangeExchange a portal link tokenGET/v1/audit/portal/eventsList events through the portalGET/v1/audit/portal/events/{id}Get an event through the portalGET/v1/audit/portal/events/{id}/verifyVerify an event through the portalGET/v1/audit/portal/orgGet the portal's org and issuerGET/v1/audit/portal/streamsList streams through the portalPOST/v1/audit/portal/streamsCreate a stream through the portalDELETE/v1/audit/portal/streams/{id}Delete a stream through the portalPOST/v1/audit/portal/streams/{id}/testTest a stream through the portal
Public proofGET/v1/proof/audit/{event_id}Get the public proof of an audit eventPOST/v1/proof/audit/{event_id}/verifyVerify a copy of an audit event
Terms
Audit orgOne end customer of yours, registered with POST /v1/audit/orgs; every event, stream, portal session and export belongs to exactly one org.
organization_idYour own id for an audit org; every endpoint also accepts the aorg_ id in its place.
aorg_ idThe internal id Invoance assigns to an audit org, returned as id and org_id.
aevt_ idAn audit event id: aevt_ followed by a ULID, minted at ingest.
seqThe event's position in its org's log, assigned by the signer in commit order, starting at 1 with no gaps.
Canonical bytesThe signed object (signed fields plus schema_id) with null members removed, keys sorted recursively and compact serialization; what payload_hash and signature are computed over.
payload_hashSHA-256 of the canonical bytes, stored with the row and returned as 64 hex characters.
Pinned keyThe tenant's Ed25519 public key in tenant_keys; every verification uses it rather than the key stored on the row.
Hot, cold, purgedAn event is hot in Postgres for the plan's hot window, then archived to cold storage until retention_days, then deleted.
evacuated_through_seqThe highest seq that retention has moved out of the hot log; the integrity scan starts above it.
StreamA webhook destination that receives an org's new events in seq batches signed with an HMAC secret.
CursorA stream's cursor_seq, the last seq delivered; it starts at the org's seq when the stream is created.
Portal linkA one-time token with a short open window, minted with POST /v1/audit/portal_sessions and spent by POST /v1/audit/portal/exchange.
Portal JWTThe org-scoped, read-only bearer token returned by the exchange, with audience audit_portal and the session's intent.
IntentWhat a portal link may do: audit_logs reads events, log_streams manages streams; neither can do the other.
ExportAn asynchronous job that writes an org's events, cold and hot, to one CSV or NDJSON file behind a 24 hour presigned URL.

Proof infrastructure. Records are hashed, signed with your organization's Ed25519 key, and stored append-only, so anyone can check them later.

Products

  • Audit Logs
  • Event Ledger
  • AI Attestation
  • Document Anchoring
  • Traces

Developers

  • Documentation
  • API reference
  • SDKs
  • How it works
  • How traces seal
  • System status

Verify

  • Audit Log
  • Event
  • AI Attestation
  • Document
  • Trace

Company

  • Why Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Resources
  • Help center
  • Contact
© 2026 Invoance
PrivacyLegal noticeLegal FAQGitHubLinkedInX