Home
Home/Developers/Audit logs/Event schema
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
Docs · Audit logs

Event schema

Every audit event uses the invoance.audit/1 envelope. This page lists its fields, the bytes that get signed, and how seq works.

Ingest endpointVerify endpoint
sha-256 · 3a352297…2592
sha-256 · e51db071…5f1f

You send an action, when it happened, who did it and what it was done to. The envelope is strict: an unknown key, a missing required field or a wrong type gets 400 and nothing is stored.

The backend adds id, org_id, seq, ingested_at and schema_id, serializes the result canonically, hashes it with SHA-256 and signs the bytes with your Ed25519 key. Every read returns the event with payload_hash, signature and signing_public_key.

Free-form data goes in metadata: flat keys with string, boolean or integer values. The envelope itself is the same for every customer.

Schema idinvoance.audit/1
Send toPOST /v1/audit/events
BodyJSON up to 64 KB; the Idempotency-Key header is required
Unknown keys400 invalid_envelope, at the top level and inside actor, targets[] and context
Duplicate keys400 duplicate_json_key, anywhere in the body
versionReserved; must be 1 when present and is not signed
Example
{
  "organization_id": "org_8472",
  "action": "user.signed_in",
  "occurred_at": "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
  }
}
Request

What you send. Each limit is the one the backend enforces; the error it returns is listed on the ingest endpoint.

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.

Stored event

What every read returns. Get, list, the portal, stream deliveries and exports all use this shape. Your organization_id is not on it; org_id is the aorg_ id.

Event 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.

What is signed

Signed object

Ten fields plus the constant schema_id. Nothing else is covered.

  • org_id, event_id, seq, ingested_at
  • action, occurred_at, actor, targets
  • context and metadata, when present and not null
  • schema_id, always invoance.audit/1

Not signed: organization_id as you sent it, version, and the payload_hash, signature and signing_public_key columns themselves.

Verification

GET /v1/audit/events/{id}/verify rebuilds the bytes from the stored row. It checks the hash, then schema_id, then the signature against your registered key, never the key on the row.

reason names the first failed check: canonicalization_failed, payload_hash_mismatch, wrong_domain, signature_invalid.

POST /v1/proof/audit/{event_id}/verify does the same for a copy you hold, without an API key. The SDK verifiers rebuild the same bytes offline; see verifying an export.

Canonical bytes
  1. Assemble

    Take the ten signed fields from the stored event and add schema_id. Nothing else goes in.

  2. Normalize timestamps

    occurred_at and ingested_at become UTC with exactly three fractional digits, truncated, and a Z suffix.

  3. Drop nulls

    A member whose value is null is removed at every depth. Absent and null give the same bytes.

  4. Sort keys

    Object keys are sorted recursively. Arrays keep their order.

  5. Serialize

    Compact UTF-8 JSON with no whitespace. Only quotes, backslashes and control characters are escaped. A float anywhere is refused.

  6. Hash and sign

    payload_hash is the SHA-256 of the bytes as lowercase hex. signature is Ed25519 over the same bytes with your registered key.

Sequence numbers

Assignment

The signer locks the org's counter, adds one and writes the event in the same transaction.

A redelivered duplicate rolls the transaction back, so no number is burned.

seq starts at 1, follows commit order and has no gaps.

Gap detection

GET /v1/audit/orgs/{id}/integrity scans the hot log from evacuated_through_seq + 1 to last_seq.

It lists every missing range. A hole below last_seq means a row was removed.

It cannot see the newest rows removed together with a rolled-back counter; the response says so in its note field.

Questions
Can I add my own top-level fields?
No. Unknown keys are rejected with 400 invalid_envelope. Put extra data in metadata, actor.metadata or targets[].metadata.
Why was a decimal rejected?
Metadata values must be strings, booleans or 64-bit integers, so a float never reaches the signed bytes. Send decimals as strings.
Which timestamp does the signature cover?
Both. occurred_at is your clock and ingested_at is the server's. Each is normalized first, so a copy with reformatted timestamps still verifies.
Do I have to send an Idempotency-Key?
Yes. Every SDK derives one when you pass none: idem_ plus the SHA-256 of the sorted compact body, so identical bodies map to one event.
Will the schema change?
Not in place. The canonical form is frozen; a wire change would ship under a new schema id, so events signed today keep verifying.

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

  • Company overview
  • What is Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Resources
  • Help center
  • Contact
© 2025 – 2026 Invoance, Inc. All rights reserved.© 2026 Invoance, Inc. All rights reserved.
PrivacyLegal noticeLegal FAQ