Audit public proof
The two public endpoints behind /proof/audit: no key, the signed envelope and a verify call for the copy someone holds.
/v1/proof/audit/{event_id}Get the public proof of an audit event
Returns the issuer, the org, the event's envelope fields and a server-computed verification verdict, without the event's actor, targets, context or metadata.
event_idThe aevt_ id of the event.
- The event's actor, targets, context and metadata are read to recompute the canonical bytes but are never serialized into the response; only the holder of a full copy can check content, via the verify route below.
- The lookup is by id across all tenants; the id itself is the only access control, and an unknown id gets 404 event_not_found.
- The route sits behind a limiter configured for 2 requests per second and 10 per minute, but that limiter keys on a request extension unauthenticated calls do not carry, so it passes public traffic through.
- Events that left the hot log for cold storage return 404 here.
// No API key: the public proof endpoint is unauthenticated.
const eventId = "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK";
const res = await fetch("https://api.invoance.com/v1/proof/audit/" + eventId);
if (!res.ok) {
throw new Error(res.status + " " + (await res.text()));
}
const proof = await res.json();
console.log(proof.organization.issuer_name, proof.event.action, proof.event.seq);
console.log(proof.verification.valid, proof.verification.reason);
{
"organization": {
"name": "Northwind Legal",
"issuer_name": "Northwind Legal Ltd",
"primary_domain": "northwindlegal.example",
"domain_verified": true,
"logo_url": "https://cdn.example.com/northwind/logo.svg"
},
"org": {
"org_id": "aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM",
"name": "Acme Robotics"
},
"event": {
"event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
"org_id": "aorg_01J0XW9K3RQ5T7V8Y2C4E6G8HM",
"seq": 42,
"schema_id": "invoance.audit/1",
"action": "user.signed_in",
"occurred_at": "2026-09-22T08:14:07.000Z",
"ingested_at": "2026-09-22T08:14:07.312Z",
"payload_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
"signature": "c6a2bf3bc3895915ead5f0b99eaf84534d4e8b9aa68bef8b0508cfd473f06e694d76d2bc330b83cfa613e49e7d2490d0413285017acfb78746bcc1524d05160d",
"signing_public_key": "bee215a9d2a0170176a88733056d8a0ae5372b0da8238796bef4a0b75d4c0974"
},
"verification": {
"valid": true,
"reason": null,
"schema": "invoance.audit/1",
"payload_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
"key_source": "tenant_keys"
}
}
organizationThe tenant that recorded the event, from its organization profile.
organization.nameOrganization name.
organization.issuer_nameName shown as the issuer on proof pages.
organization.primary_domainPrimary domain registered for the organization.
organization.domain_verifiedTrue when the primary domain passed DNS verification.
organization.logo_urlLogo URL, or null when none is set.
orgThe audit org the event belongs to.
org.org_idThe aorg_ id.
org.nameThe org's display name, or null.
eventEnvelope fields only; the content fields are used for the verdict but never returned.
event.event_idThe aevt_ id.
event.org_idThe aorg_ id.
event.seqPosition in the org's log.
event.schema_idAlways invoance.audit/1. One of invoance.audit/1.
event.actionThe action string.
event.occurred_atCanonical occurred_at, UTC with three fractional digits.
event.ingested_atCanonical ingested_at.
event.payload_hashStored SHA-256 of the canonical bytes.
event.signatureStored Ed25519 signature.
event.signing_public_keyKey recorded with the row, for display; the verdict uses tenant_keys.
verificationVerdict computed on the server when the call is made.
verification.validTrue when hash, domain tag and signature all check out against the tenant's registered key.
verification.reasonFirst failed check, or null. One of canonicalization_failed, payload_hash_mismatch, wrong_domain, signature_invalid.
verification.schemaAlways invoance.audit/1. One of invoance.audit/1.
verification.payload_hashRecomputed hash; equals event.payload_hash when the row is intact.
verification.key_sourceAlways tenant_keys. One of tenant_keys.
event_not_foundNo audit event with that id exists in any tenant.
db_errorA database query failed.
key_unavailableThe event's tenant has no registered key in tenant_keys.
/v1/proof/audit/{event_id}/verifyVerify a copy of an audit event
Takes the full event JSON you hold, rebuilds its canonical bytes on the server, and reports whether it reproduces the anchored hash and whether the anchored signature verifies over it.
Content-Typeapplication/json; the body is parsed as JSON regardless of the header.
event_idThe aevt_ id of the anchored event to compare against.
eventThe event as returned by get, list, the portal, a stream delivery or an export; the bare event object is also accepted as the whole body.
event.idThe aevt_ id; copied to event_id for the recompute when event_id is absent.
event.org_idThe aorg_ id, part of the signed bytes.
event.seqPart of the signed bytes.
event.ingested_atPart of the signed bytes; any RFC 3339 form is normalized.
event.actionPart of the signed bytes.
event.occurred_atPart of the signed bytes; any RFC 3339 form is normalized.
event.actorPart of the signed bytes.
event.targetsPart of the signed bytes.
event.contextPart of the signed bytes when present and not null.
event.metadataPart of the signed bytes when present and not null; null values inside are dropped.
- Extra keys on the copy (payload_hash, signature, signing_public_key, schema_id) are ignored; only the signed fields feed the recompute, so a copy from any surface verifies as long as its signed fields are unchanged.
- A copy that cannot be canonicalized (missing required field, float in metadata) is reported as a failed check with reason canonicalization_failed, not as a 4xx.
- The submitted copy is used only for the recompute; it is neither stored nor echoed back.
- Any RFC 3339 form of the timestamps is accepted because they are normalized before hashing, so a copy whose timestamps were reformatted still matches.
- None of the SDKs call this route; they verify offline instead with verifyAuditEvent / verify_audit_event, which implements the same canonicalization.
import { readFile } from "node:fs/promises";
// No API key: the public verify endpoint is unauthenticated.
// event.json holds the event exactly as get, an export or a stream delivered it.
const event = JSON.parse(await readFile("event.json", "utf8"));
const res = await fetch("https://api.invoance.com/v1/proof/audit/" + event.id + "/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event }),
});
if (!res.ok) {
throw new Error(res.status + " " + (await res.text()));
}
const result = await res.json();
console.log(result.match_result, result.signature_valid, result.reason);
{
"event_id": "aevt_01J0Y1Z2A3B4C5D6E7F8G9H0JK",
"match_result": true,
"signature_valid": true,
"reason": null,
"anchored_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
"submitted_hash": "df929158c7ce2107eff769fbcd58376c1d84dee0b5212b314f6f423dd20534d3",
"anchored_at": "2026-09-22T08:14:07.312+00:00",
"schema": "invoance.audit/1",
"key_source": "tenant_keys"
}
event_idThe id from the path.
match_resultTrue when the submitted copy's canonical hash equals the anchored payload_hash.
signature_validTrue when the bytes carry the audit domain tag and the anchored signature verifies over the submitted canonical bytes under the tenant's registered key.
reasonFirst failed check, or null when both flags are true. One of canonicalization_failed, payload_hash_mismatch, wrong_domain, signature_invalid.
anchored_hashThe payload_hash stored for the event.
submitted_hashSHA-256 of the canonical bytes rebuilt from your copy; empty when it could not be canonicalized.
anchored_atThe stored ingested_at, written with a +00:00 offset.
schemaAlways invoance.audit/1. One of invoance.audit/1.
key_sourceAlways tenant_keys. One of tenant_keys.
invalid_jsonThe body is not valid JSON or contains a duplicate key in any object.
invalid_eventNeither the body nor its event key is a JSON object.
event_not_foundNo audit event with that id exists in any tenant.
db_errorA database query failed.
key_unavailableThe event's tenant has no registered key in tenant_keys.
/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/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/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/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