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 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 · 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

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