Event schema (invoance.audit/1)
Every event you send to POST /v1/audit/events uses the frozen invoance.audit/1 envelope: a fixed set of fields, fixed types, within the limits below. The envelope is strict and identical for every customer, so an unknown top-level field, a missing required field, or a wrong type is rejected 400 and never stored or signed. What is not constrained is the contents of metadata: you send whatever key/values you want within the size limits, with no pre-declaration.
A complete event
Exactly what you send, with every field populated. This one record carries an actor with its own metadata, two targets (one with metadata, one without), request context, and top-level metadata using all three allowed scalar types, a string, an integer, and a boolean. The server adds id, org_id, seq, ingested_at and schema_id, then signs it.
POST /v1/audit/eventscurl -X POST https://api.invoance.com/v1/audit/events \
-H "Authorization: Bearer invoance_live_xxx" \
-H "Idempotency-Key: 7f4c1c9d-5b8a-4d42-9e0b-1f2a3b4c5d6e" \
-H "Content-Type: application/json" \
-d '{
"organization_id": "org_01J8F3KQ2R7VWX9YB4ND6MCZAH",
"action": "team.member.invited",
"occurred_at": "2026-06-24T12:00:00.000Z",
"actor": {
"type": "user",
"id": "user_42",
"name": "Ada Lovelace",
"metadata": { "role": "admin", "mfa": true }
},
"targets": [
{
"type": "user",
"id": "user_99",
"name": "Charles Babbage",
"metadata": { "invited_email": "charles@acme.com" }
},
{ "type": "team", "id": "team_eng", "name": "Engineering" }
],
"context": { "location": "203.0.113.10", "user_agent": "Chrome/124.0.0.0" },
"metadata": { "plan": "growth", "seats": 25, "trial": false },
"version": 1
}'invoance.audit/1·version 1 (reserved; if you send a version it must equal 1)Fields
| Field | Type | Required | Notes |
|---|---|---|---|
| action | string | Yes | Dot-segmented resource.verb, e.g. user.signed_in or team.member.invited. |
| occurred_at | string (RFC3339 UTC) | Yes | When it happened, on your clock. Bounded to now − 5y … now + 24h. SDKs default this to now. |
| actor | object | Yes | Who did it: type (user | api_key | system) and id required; name and metadata optional. |
| targets | array of object | Yes | What it was done to. May be empty ([]) but the field must be present. Each: type, id, optional name and metadata. |
| context | object | No | location = actor IP, user_agent = client string. |
| metadata | object | No | Flat key to scalar map (see limits). Free-form contents. |
| version | integer | No | Reserved. Must equal 1 if present. |
Absent optional fields, or fields sent as null, are omitted from the signed bytes entirely, so the signature covers exactly the fields present. The server assigns id (aevt_<ULID>), org_id, ingested_at, seq and schema_id; client-sent values for those are ignored.
Limits and rules
- metadata (and actor / target metadata): ≤ 50 keys, key ≤ 40 chars, value ≤ 500 chars.
- metadata values are scalars only: string, boolean, or int64. Floats and nested objects/arrays are rejected 400 (send decimals as strings).
- Idempotency-Key header is required on ingest; the SDKs derive one from the event content.
- Ids: orgs are
aorg_<ULID>, events areaevt_<ULID>.
What gets signed
The canonical bytes of the present fields are signed with your tenant's Ed25519 key. The /verify endpoint and the SDK offline verifier rebuild those exact bytes and check the signature against your pinned key. The signature attests ingested_at (our server clock), not the client-supplied occurred_at.