Home
Home/Developers/Events/hashing
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 schemaVerifying attestations
Traces
Sealing a trace
Audit logs
OrganizationsStreamsPortalPublic proofEvent schemaExporting eventsIntegrationsEmbeddable viewer
All endpoints
Reference
SDKs
Verification
Docs · Events

Canonical JSON and the three hashes

What the backend hashes when you send an event, in what order, and how to compute payload_hash yourself.

Events APIVerify endpoint
sha-256 · 3a352297…2592
sha-256 · 6cd46d8c…3388
What is hashed and when

Two hashes are computed before the 201 and one after. The payload is stored as sent; every hash is computed from it.

  1. 01 · Your appPOST /v1/eventsevent_type, payload, optional event_time and Idempotency-Key
  2. 02 · CanonicalizeSort keys, drop whitespacepayload_hash = SHA-256 of these bytes
  3. 03 · Envelopeevent_id, ingested_atrequest_hash = SHA-256 of the envelope
  4. 04 · Reply201 Createdevent_id and ingested_at; the event is on the queue
  5. 05 · WorkerSign and writeevent_hash, Ed25519 signature, one ledger row
  1. Request

    The handler checks the key's write scope and the monthly events quota, then trims event_type. Nothing is hashed yet.

  2. Canonicalize and hash

    The payload is canonicalized and serialized. More than 64 KB is 400 payload_too_large. SHA-256 of the bytes is payload_hash.

  3. Envelope and reply

    A fresh event_id and ingested_at go into an envelope with the payload as sent. Its SHA-256 is request_hash. The envelope is queued and the 201 returns event_id and ingested_at.

  4. Sign and write

    A worker signs compact JSON of v, event_id, tenant_id, event_type, payload_hash, request_hash and ingested_at with your Ed25519 key. It computes event_hash from the payload in sent order and inserts the row once; a redelivered message writes nothing. Until the row exists, get and verify return 404.

Canonical JSON

The handler parses the payload, rewrites it with canonicalize_json and serializes it again. The same function runs when you send payload to verify.

  • Object keys are sorted by Unicode code point at every depth. Uppercase sorts before lowercase, and "10" before "9".
  • Arrays keep their order. Each element is canonicalized in turn.
  • No whitespace. Members are joined with a comma and a colon only.
  • Strings are written as UTF-8. Only quotes, backslashes and control characters are escaped; an escaped input such as \u00e9 comes out as the character.
  • Integers are written as sent. Floats are rewritten in shortest form: 1.50 becomes 1.5 and 1e3 becomes 1000.0.
  • null stays. Nothing is dropped, so {"a":null} and {} hash differently.
  • A key sent twice keeps its last value.
  • The 64 KB payload limit is measured on these bytes.
{
  "policy_id": "pol_8472",
  "decision": "approved",
  "approved_by": {
    "team": "risk_committee",
    "members": ["u_42", "u_7"],
    "chair": null
  },
  "score": 1.50,
  "note": "caf\u00e9 r\u00e9sum\u00e9"
}
The three hashes
payload_hashSHA-256 of the canonical payload bytes, hex. Computed in the handler before the 201. The hash you can reproduce, and the value verify reports as anchored_hash.
event_hashSHA-256 of the payload written back as compact JSON in the key order it arrived, without sorting. Computed by the worker. Key order changes it and never changes payload_hash. Get reads the payload back from JSONB, so its key order can differ from what you sent.
request_hashSHA-256 of the compact envelope: v (1), event_id, tenant_id, api_key_id, user_id (null for a key), ingested_at, event_type, event_time (null when not sent), payload as sent, idempotency_key (null when not sent) and payload_hash, in that order. Fresh event_id and ingested_at make it unique per ingest. The ledger's unique index on it turns a redelivered message into a no-op.
Idempotency-Key checkNot stored on the event. SHA-256 of event_type, then event_time as RFC 3339 when sent, then the canonical payload bytes, concatenated with nothing between. A reused Idempotency-Key is checked against it.

What verify compares

POST /v1/events/{event_id}/verify takes payload_hash or payload, never both.

  • The submitted hash is compared with payload_hash, then request_hash, then event_hash. matched_field names the first match, null when none.
  • With payload, the backend canonicalizes and hashes it for you and method is payload. With payload_hash, method is hash.
  • anchored_hash is always the stored payload_hash, whether or not it matched.
  • Your own compact serialization in the original key order matches event_hash only when your bytes equal the worker's.

This is a hash check. The signature, the public key and the signed bytes are on GET /v1/proof/event/{event_id}.

{
  "event_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "tenant_id": "0a4f6d18-2b9c-4e7a-8d31-6c5e9f2a7b40",
  "event_type": "policy.approval",
  "payload": {
    "policy_id": "pol_8472",
    "approved_by": "risk_committee",
    "decision": "approved"
  },
  "event_time": "2026-09-22T08:14:07Z",
  "retention_policy": "standard",
  "expires_at": "2027-09-22T08:14:07Z",
  "api_key_id": "5e2c8b7a-1d4f-4c93-a6e0-9b3f7d2c1e58",
  "ingested_at": "2026-09-22T08:14:07Z",
  "payload_hash": "f3c57489bbda1ac2876ce8cef9f7778e07ce2eecd5021907ad2ca2cc3895011d",
  "request_hash": "15142ed07f2ed8fc3f7a50ba6d980d2455ad35ba95d8744759b7afe53f092402",
  "event_hash": "9831ee77aaae90c0b9cbf5d731c3285967ed73847d9ad7e0d8ec0a39de3c09c7",
  "idempotency_key": "policy-approval-pol_8472",
  "organization": {
    "name": "Northwind Compliance",
    "issuer_name": "Northwind Compliance Ltd",
    "primary_domain": "northwind.example",
    "domain_verified": true,
    "domain_verified_at": "2026-08-30T11:20:15Z",
    "logo_url": "https://cdn.invoance.com/logos/northwind.png"
  }
}
Reproduce payload_hash

Standard library only. Canonicalize, hash, then compare with payload_hash from GET /v1/events/{event_id}. Node prints true and Python prints True for the example event.

  • Node: build the string yourself. JSON.stringify on a sorted object lists integer-like keys first in numeric order, so "9" lands ahead of "10".
  • Node writes 1.0 as 1 and the backend writes 1.0. Python writes 0.00001 as 1e-05 and the backend writes 0.00001.
  • Strings and integers up to 2^53 match in both. Node rounds larger integers; the backend keeps them.
  • For a payload with floats, send payload to the verify endpoint and let the backend canonicalize.
import { createHash } from "node:crypto";

// The backend's rules: keys sorted by code point at every depth, arrays in
// order, no whitespace. Built as a string on purpose: a JS object lists
// integer-like keys first in numeric order, so "9" would land ahead of "10".
function canonical(value) {
  if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
  if (value !== null && typeof value === "object") {
    const members = Object.keys(value)
      .sort()
      .map((key) => JSON.stringify(key) + ":" + canonical(value[key]));
    return "{" + members.join(",") + "}";
  }
  return JSON.stringify(value);
}

const payload = { policy_id: "pol_8472", approved_by: "risk_committee", decision: "approved" };
const payloadHash = createHash("sha256").update(canonical(payload), "utf8").digest("hex");

const response = await fetch("https://api.invoance.com/v1/events/7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4", {
  headers: { Authorization: "Bearer " + process.env.INVOANCE_API_KEY },
});
const event = await response.json();

console.log(payloadHash === event.payload_hash); // true
EndpointsFields, errors and samples
POST/v1/eventsIngest an eventGET/v1/eventsList eventsGET/v1/events/{event_id}Get an eventPOST/v1/events/{event_id}/verifyVerify an event

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